Dodan divert za squirrelmail/functions/strings.php zbog problema s PHP/SquirrelMail.
[squirrelmail-cn.git] / squirrelmail / functions / strings.php
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-2012 The SquirrelMail Project Team
10  * @license http://opensource.org/licenses/gpl-license.php GNU Public License
11  * @version $Id: strings.php 14274 2012-02-07 23:04:45Z pdontthink $
12  * @package squirrelmail
13  */
14
15 /**
16  * SquirrelMail version number -- DO NOT CHANGE
17  */
18 global $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  */
25 global $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  */
33 require_once(SM_PATH . 'functions/global.php');
34
35 if (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, &#039 comes up as 5 characters instead of 1.
46  * This should not add newlines to the end of lines.
47  */
48 function 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  */
105 function 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   */
167 function 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  */
240 function 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  */
266 function php_self() {
267
268     $request_uri = '';
269
270     // first try $_SERVER['PHP_SELF'], which seems most reliable
271     // (albeit it usually won't include the query string)
272     //
273     $request_uri = '';
274     if (!sqgetGlobalVar('PHP_SELF', $request_uri, SQ_SERVER)
275      || empty($request_uri)) {
276
277         // well, then let's try $_SERVER['REQUEST_URI']
278         //
279         $request_uri = '';
280         if (!sqgetGlobalVar('REQUEST_URI', $request_uri, SQ_SERVER)
281          || empty($request_uri)) {
282
283             // TODO: anyone have any other ideas?  maybe $_SERVER['SCRIPT_NAME']???
284             //
285             return '';
286         }
287
288     }
289
290     // we may or may not have any query arguments, depending on
291     // which environment variable was used above, and the PHP
292     // version, etc., so let's check for it now
293     //
294     $query_string = '';
295     if (strpos($request_uri, '?') === FALSE
296      && sqgetGlobalVar('QUERY_STRING', $query_string, SQ_SERVER)
297      && !empty($query_string)) {
298
299         $request_uri .= '?' . $query_string;
300     }
301
302     return $request_uri;
303
304 }
305
306
307 /**
308  * Find out where squirrelmail lives and try to be smart about it.
309  * The only problem would be when squirrelmail lives in directories
310  * called "src", "functions", or "plugins", but people who do that need
311  * to be beaten with a steel pipe anyway.
312  *
313  * @return string the base uri of squirrelmail installation.
314  */
315 function sqm_baseuri(){
316     global $base_uri, $PHP_SELF;
317     /**
318      * If it is in the session, just return it.
319      */
320     if (sqgetGlobalVar('base_uri',$base_uri,SQ_SESSION)){
321         return $base_uri;
322     }
323     $dirs = array('|src/.*|', '|plugins/.*|', '|functions/.*|');
324     $repl = array('', '', '');
325     $base_uri = preg_replace($dirs, $repl, $PHP_SELF);
326     return $base_uri;
327 }
328
329 /**
330  * get_location
331  *
332  * Determines the location to forward to, relative to your server.
333  * This is used in HTTP Location: redirects.
334  * If set, it uses $config_location_base as the first part of the URL,
335  * specifically, the protocol, hostname and port parts. The path is
336  * always autodetected.
337  *
338  * @return string the base url for this SquirrelMail installation
339  */
340 function get_location () {
341
342     global $imap_server_type, $config_location_base,
343            $is_secure_connection, $sq_ignore_http_x_forwarded_headers;
344
345     /* Get the path, handle virtual directories */
346     if(strpos(php_self(), '?')) {
347         $path = substr(php_self(), 0, strpos(php_self(), '?'));
348     } else {
349         $path = php_self();
350     }
351     $path = substr($path, 0, strrpos($path, '/'));
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  */
430 function 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  */
460 function 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  */
492 function sq_mt_seed($Val) {
493     /* if mt_getrandmax() does not return a 2^n - 1 number,
494        this might not work well.  This uses $Max as a bitmask. */
495     $Max = mt_getrandmax();
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  */
522 function 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  */
565 function 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  * @return string the filesize in human readable format
581  */
582 function show_readable_size($bytes) {
583     $bytes /= 1024;
584     $type = 'k';
585
586     if ($bytes / 1024 > 1) {
587         $bytes /= 1024;
588         $type = 'M';
589     }
590
591     if ($bytes < 10) {
592         $bytes *= 10;
593         settype($bytes, 'integer');
594         $bytes /= 10;
595     } else {
596         settype($bytes, 'integer');
597     }
598
599     return $bytes . '<small>&nbsp;' . $type . '</small>';
600 }
601
602 /**
603  * Generates a random string from the caracter set you pass in
604  *
605  * @param int size the size of the string to generate
606  * @param string chars a string containing the characters to use
607  * @param int flags a flag to add a specific set to the characters to use:
608  *     Flags:
609  *       1 = add lowercase a-z to $chars
610  *       2 = add uppercase A-Z to $chars
611  *       4 = add numbers 0-9 to $chars
612  * @return string the random string
613  */
614 function GenerateRandomString($size, $chars, $flags = 0) {
615     if ($flags & 0x1) {
616         $chars .= 'abcdefghijklmnopqrstuvwxyz';
617     }
618     if ($flags & 0x2) {
619         $chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
620     }
621     if ($flags & 0x4) {
622         $chars .= '0123456789';
623     }
624
625     if (($size < 1) || (strlen($chars) < 1)) {
626         return '';
627     }
628
629     sq_mt_randomize(); /* Initialize the random number generator */
630
631     $String = '';
632     $j = strlen( $chars ) - 1;
633     while (strlen($String) < $size) {
634         $String .= $chars{mt_rand(0, $j)};
635     }
636
637     return $String;
638 }
639
640 /**
641  * Escapes special characters for use in IMAP commands.
642  *
643  * @param string the string to escape
644  * @return string the escaped string
645  */
646 function quoteimap($str) {
647     // FIXME use this performance improvement (not changing because this is STABLE branch): return str_replace(array('\\', '"'), array('\\\\', '\\"'), $str);
648     return preg_replace("/([\"\\\\])/", "\\\\$1", $str);
649 }
650
651 /**
652  * Trims array
653  *
654  * Trims every element in the array, ie. remove the first char of each element
655  * Obsolete: will probably removed soon
656  * @param array array the array to trim
657  * @obsolete
658  */
659 function TrimArray(&$array) {
660     foreach ($array as $k => $v) {
661         global $$k;
662         if (is_array($$k)) {
663             foreach ($$k as $k2 => $v2) {
664                 $$k[$k2] = substr($v2, 1);
665             }
666         } else {
667             $$k = substr($v, 1);
668         }
669
670         /* Re-assign back to array. */
671         $array[$k] = $$k;
672     }
673 }
674
675 /**
676  * Removes slashes from every element in the array
677  */
678 function RemoveSlashes(&$array) {
679     foreach ($array as $k => $v) {
680         global $$k;
681         if (is_array($$k)) {
682             foreach ($$k as $k2 => $v2) {
683                 $newArray[stripslashes($k2)] = stripslashes($v2);
684             }
685             $$k = $newArray;
686         } else {
687             $$k = stripslashes($v);
688         }
689
690         /* Re-assign back to the array. */
691         $array[$k] = $$k;
692     }
693 }
694
695 /**
696  * Create compose link
697  *
698  * Returns a link to the compose-page, taking in consideration
699  * the compose_in_new and javascript settings.
700  * @param string url the URL to the compose page
701  * @param string text the link text, default "Compose"
702  * @return string a link to the compose page
703  */
704 function makeComposeLink($url, $text = null, $target='')
705 {
706     global $compose_new_win,$javascript_on;
707
708     if(!$text) {
709         $text = _("Compose");
710     }
711
712
713     // if not using "compose in new window", make
714     // regular link and be done with it
715     if($compose_new_win != '1') {
716         return makeInternalLink($url, $text, $target);
717     }
718
719
720     // build the compose in new window link...
721
722
723     // if javascript is on, use onClick event to handle it
724     if($javascript_on) {
725         sqgetGlobalVar('base_uri', $base_uri, SQ_SESSION);
726         return '<a href="javascript:void(0)" onclick="comp_in_new(\''.$base_uri.$url.'\')">'. $text.'</a>';
727     }
728
729
730     // otherwise, just open new window using regular HTML
731     return makeInternalLink($url, $text, '_blank');
732
733 }
734
735 /**
736  * Print variable
737  *
738  * sm_print_r($some_variable, [$some_other_variable [, ...]]);
739  *
740  * Debugging function - does the same as print_r, but makes sure special
741  * characters are converted to htmlentities first.  This will allow
742  * values like <some@email.address> to be displayed.
743  * The output is wrapped in <<pre>> and <</pre>> tags.
744  *
745  * @return void
746  */
747 function sm_print_r() {
748     ob_start();  // Buffer output
749     foreach(func_get_args() as $var) {
750         print_r($var);
751         echo "\n";
752     }
753     $buffer = ob_get_contents(); // Grab the print_r output
754     ob_end_clean();  // Silently discard the output & stop buffering
755     print '<pre>';
756     print htmlentities($buffer);
757     print '</pre>';
758 }
759
760 /**
761  * version of fwrite which checks for failure
762  */
763 function sq_fwrite($fp, $string) {
764         // write to file
765         $count = @fwrite($fp,$string);
766         // the number of bytes written should be the length of the string
767         if($count != strlen($string)) {
768                 return FALSE;
769         }
770
771         return $count;
772 }
773 /**
774  * Tests if string contains 8bit symbols.
775  *
776  * If charset is not set, function defaults to default_charset.
777  * $default_charset global must be set correctly if $charset is
778  * not used.
779  * @param string $string tested string
780  * @param string $charset charset used in a string
781  * @return bool true if 8bit symbols are detected
782  * @since 1.5.1 and 1.4.4
783  */
784 function sq_is8bit($string,$charset='') {
785     global $default_charset;
786
787     if ($charset=='') $charset=$default_charset;
788
789     /**
790      * Don't use \240 in ranges. Sometimes RH 7.2 doesn't like it.
791      * Don't use \200-\237 for iso-8859-x charsets. This ranges
792      * stores control symbols in those charsets.
793      * Use preg_match instead of ereg in order to avoid problems
794      * with mbstring overloading
795      */
796     if (preg_match("/^iso-8859/i",$charset)) {
797         $needle='/\240|[\241-\377]/';
798     } else {
799         $needle='/[\200-\237]|\240|[\241-\377]/';
800     }
801     return preg_match("$needle",$string);
802 }
803
804 /**
805  * Function returns number of characters in string.
806  *
807  * Returned number might be different from number of bytes in string,
808  * if $charset is multibyte charset. Detection depends on mbstring
809  * functions. If mbstring does not support tested multibyte charset,
810  * vanilla string length function is used.
811  * @param string $str string
812  * @param string $charset charset
813  * @since 1.5.1 and 1.4.6
814  * @return integer number of characters in string
815  */
816 function sq_strlen($string, $charset=NULL){
817
818    // NULL charset?  Just use strlen()
819    //
820    if (is_null($charset))
821       return strlen($string);
822
823
824    // use current character set?
825    //
826    if ($charset == 'auto')
827    {
828 //FIXME: this may or may not be better as a session value instead of a global one
829       global $sq_string_func_auto_charset;
830       if (!isset($sq_string_func_auto_charset))
831       {
832          global $default_charset, $squirrelmail_language;
833          set_my_charset();
834          $sq_string_func_auto_charset = $default_charset;
835          if ($squirrelmail_language == 'ja_JP') $sq_string_func_auto_charset = 'euc-jp';
836       }
837       $charset = $sq_string_func_auto_charset;
838    }
839
840
841    // standardize character set name
842    //
843    $charset = strtolower($charset);
844
845
846 /* ===== FIXME: this list is not used in 1.5.x, but if we need it, unless this differs between all our string function wrappers, we should store this info in the session
847    // only use mbstring with the following character sets
848    //
849    $sq_strlen_mb_charsets = array(
850       'utf-8',
851       'big5',
852       'gb2312',
853       'gb18030',
854       'euc-jp',
855       'euc-cn',
856       'euc-tw',
857       'euc-kr'
858    );
859
860
861    // now we can use mb_strlen() if needed
862    //
863    if (in_array($charset, $sq_strlen_mb_charsets)
864     && in_array($charset, sq_mb_list_encodings()))
865 ===== */
866 //FIXME: is there any reason why this cannot be a static global array used by all string wrapper functions?
867    if (in_array($charset, sq_mb_list_encodings()))
868       return mb_strlen($string, $charset);
869
870
871    // else use normal strlen()
872    //
873    return strlen($string);
874
875 }
876
877 /**
878   * This is a replacement for PHP's strpos() that is
879   * multibyte-aware.
880   *
881   * @param string $haystack The string to search within
882   * @param string $needle   The substring to search for
883   * @param int    $offset   The offset from the beginning of $haystack
884   *                         from which to start searching
885   *                         (OPTIONAL; default none)
886   * @param string $charset  The charset of the given string.  A value of NULL
887   *                         here will force the use of PHP's standard strpos().
888   *                         (OPTIONAL; default is "auto", which indicates that
889   *                         the user's current charset should be used).
890   *
891   * @return mixed The integer offset of the next $needle in $haystack,
892   *               if found, or FALSE if not found
893   *
894   */
895 function sq_strpos($haystack, $needle, $offset=0, $charset='auto')
896 {
897
898    // NULL charset?  Just use strpos()
899    //
900    if (is_null($charset))
901       return strpos($haystack, $needle, $offset);
902
903
904    // use current character set?
905    //
906    if ($charset == 'auto')
907    {
908 //FIXME: this may or may not be better as a session value instead of a global one
909       global $sq_string_func_auto_charset;
910       if (!isset($sq_string_func_auto_charset))
911       {
912          global $default_charset, $squirrelmail_language;
913          set_my_charset();
914          $sq_string_func_auto_charset = $default_charset;
915          if ($squirrelmail_language == 'ja_JP') $sq_string_func_auto_charset = 'euc-jp';
916       }
917       $charset = $sq_string_func_auto_charset;
918    }
919
920
921    // standardize character set name
922    //
923    $charset = strtolower($charset);
924
925
926 /* ===== FIXME: this list is not used in 1.5.x, but if we need it, unless this differs between all our string function wrappers, we should store this info in the session
927    // only use mbstring with the following character sets
928    //
929    $sq_strpos_mb_charsets = array(
930       'utf-8',
931       'big5',
932       'gb2312',
933       'gb18030',
934       'euc-jp',
935       'euc-cn',
936       'euc-tw',
937       'euc-kr'
938    );
939
940
941    // now we can use mb_strpos() if needed
942    //
943    if (in_array($charset, $sq_strpos_mb_charsets)
944     && in_array($charset, sq_mb_list_encodings()))
945 ===== */
946 //FIXME: is there any reason why this cannot be a static global array used by all string wrapper functions?
947    if (in_array($charset, sq_mb_list_encodings()))
948        return mb_strpos($haystack, $needle, $offset, $charset);
949
950
951    // else use normal strpos()
952    //
953    return strpos($haystack, $needle, $offset);
954
955 }
956
957 /**
958   * This is a replacement for PHP's substr() that is
959   * multibyte-aware.
960   *
961   * @param string $string  The string to operate upon
962   * @param int    $start   The offset at which to begin substring extraction
963   * @param int    $length  The number of characters after $start to return
964   *                        NOTE that if you need to specify a charset but
965   *                        want to achieve normal substr() behavior where
966   *                        $length is not specified, use NULL (OPTIONAL;
967   *                        default from $start to end of string)
968   * @param string $charset The charset of the given string.  A value of NULL
969   *                        here will force the use of PHP's standard substr().
970   *                        (OPTIONAL; default is "auto", which indicates that
971   *                        the user's current charset should be used).
972   *
973   * @return string The desired substring
974   *
975   * Of course, you can use more advanced (e.g., negative) values
976   * for $start and $length as needed - see the PHP manual for more
977   * information:  http://www.php.net/manual/function.substr.php
978   *
979   */
980 function sq_substr($string, $start, $length=NULL, $charset='auto')
981 {
982
983    // if $length is NULL, use the full string length...
984    // we have to do this to mimick the use of substr()
985    // where $length is not given
986    //
987    if (is_null($length))
988       $length = sq_strlen($length, $charset);
989
990    
991    // NULL charset?  Just use substr()
992    //
993    if (is_null($charset))
994       return substr($string, $start, $length);
995
996
997    // use current character set?
998    //
999    if ($charset == 'auto')
1000    {
1001 //FIXME: this may or may not be better as a session value instead of a global one
1002       global $sq_string_func_auto_charset;
1003       if (!isset($sq_string_func_auto_charset))
1004       {
1005          global $default_charset, $squirrelmail_language;
1006          set_my_charset();
1007          $sq_string_func_auto_charset = $default_charset;
1008          if ($squirrelmail_language == 'ja_JP') $sq_string_func_auto_charset = 'euc-jp';
1009       }
1010       $charset = $sq_string_func_auto_charset;
1011    }
1012
1013
1014    // standardize character set name
1015    //
1016    $charset = strtolower($charset);
1017
1018
1019 /* ===== FIXME: this list is not used in 1.5.x, but if we need it, unless this differs between all our string function wrappers, we should store this info in the session
1020    // only use mbstring with the following character sets
1021    //
1022    $sq_substr_mb_charsets = array(
1023       'utf-8',
1024       'big5',
1025       'gb2312',
1026       'gb18030',
1027       'euc-jp',
1028       'euc-cn',
1029       'euc-tw',
1030       'euc-kr'
1031    );
1032
1033
1034    // now we can use mb_substr() if needed
1035    //
1036    if (in_array($charset, $sq_substr_mb_charsets)
1037     && in_array($charset, sq_mb_list_encodings()))
1038 ===== */
1039 //FIXME: is there any reason why this cannot be a global array used by all string wrapper functions?
1040    if (in_array($charset, sq_mb_list_encodings()))
1041       return mb_substr($string, $start, $length, $charset);
1042
1043
1044    // else use normal substr()
1045    //
1046    return substr($string, $start, $length);
1047
1048 }
1049
1050 /**
1051   * This is a replacement for PHP's substr_replace() that is
1052   * multibyte-aware.
1053   *
1054   * @param string $string      The string to operate upon
1055   * @param string $replacement The string to be inserted
1056   * @param int    $start       The offset at which to begin substring replacement
1057   * @param int    $length      The number of characters after $start to remove
1058   *                            NOTE that if you need to specify a charset but
1059   *                            want to achieve normal substr_replace() behavior
1060   *                            where $length is not specified, use NULL (OPTIONAL;
1061   *                            default from $start to end of string)
1062   * @param string $charset     The charset of the given string.  A value of NULL
1063   *                            here will force the use of PHP's standard substr().
1064   *                            (OPTIONAL; default is "auto", which indicates that
1065   *                            the user's current charset should be used).
1066   *
1067   * @return string The manipulated string
1068   *
1069   * Of course, you can use more advanced (e.g., negative) values
1070   * for $start and $length as needed - see the PHP manual for more
1071   * information:  http://www.php.net/manual/function.substr-replace.php
1072   *
1073   */
1074 function sq_substr_replace($string, $replacement, $start, $length=NULL,
1075                            $charset='auto')
1076 {
1077
1078    // NULL charset?  Just use substr_replace()
1079    //
1080    if (is_null($charset))
1081       return is_null($length) ? substr_replace($string, $replacement, $start)
1082                               : substr_replace($string, $replacement, $start, $length);
1083
1084
1085    // use current character set?
1086    //
1087    if ($charset == 'auto')
1088    {
1089 //FIXME: this may or may not be better as a session value instead of a global one
1090       $charset = $auto_charset;
1091       global $sq_string_func_auto_charset;
1092       if (!isset($sq_string_func_auto_charset))
1093       {
1094          global $default_charset, $squirrelmail_language;
1095          set_my_charset();
1096          $sq_string_func_auto_charset = $default_charset;
1097          if ($squirrelmail_language == 'ja_JP') $sq_string_func_auto_charset = 'euc-jp';
1098       }
1099       $charset = $sq_string_func_auto_charset;
1100    }
1101
1102
1103    // standardize character set name
1104    //
1105    $charset = strtolower($charset);
1106
1107
1108 /* ===== FIXME: this list is not used in 1.5.x, but if we need it, unless this differs between all our string function wrappers, we should store this info in the session
1109    // only use mbstring with the following character sets
1110    //
1111    $sq_substr_replace_mb_charsets = array(
1112       'utf-8',
1113       'big5',
1114       'gb2312',
1115       'gb18030',
1116       'euc-jp',
1117       'euc-cn',
1118       'euc-tw',
1119       'euc-kr'
1120    );
1121
1122
1123    // now we can use our own implementation using
1124    // mb_substr() and mb_strlen() if needed
1125    //
1126    if (in_array($charset, $sq_substr_replace_mb_charsets)
1127     && in_array($charset, sq_mb_list_encodings()))
1128 ===== */
1129 //FIXME: is there any reason why this cannot be a global array used by all string wrapper functions?
1130    if (in_array($charset, sq_mb_list_encodings()))
1131    {
1132
1133       $string_length = mb_strlen($string, $charset);
1134
1135       if ($start < 0)
1136          $start = max(0, $string_length + $start);
1137
1138       else if ($start > $string_length)
1139          $start = $string_length;
1140
1141       if ($length < 0)
1142          $length = max(0, $string_length - $start + $length);
1143
1144       else if (is_null($length) || $length > $string_length)
1145          $length = $string_length;
1146
1147       if ($start + $length > $string_length)
1148          $length = $string_length - $start;
1149
1150       return mb_substr($string, 0, $start, $charset)
1151            . $replacement
1152            . mb_substr($string,
1153                        $start + $length,
1154                        $string_length, // FIXME: I can't see why this is needed:  - $start - $length,
1155                        $charset);
1156
1157    }
1158
1159
1160    // else use normal substr_replace()
1161    //
1162    return is_null($length) ? substr_replace($string, $replacement, $start)
1163                            : substr_replace($string, $replacement, $start, $length);
1164
1165 }
1166
1167 /**
1168  * Replacement of mb_list_encodings function
1169  *
1170  * This function provides replacement for function that is available only
1171  * in php 5.x. Function does not test all mbstring encodings. Only the ones
1172  * that might be used in SM translations.
1173  *
1174  * Supported strings are stored in session in order to reduce number of
1175  * mb_internal_encoding function calls.
1176  *
1177  * If mb_list_encodings() function is present, code uses it. Main difference
1178  * from original function behaviour - array values are lowercased in order to
1179  * simplify use of returned array in in_array() checks.
1180  *
1181  * If you want to test all mbstring encodings - fill $list_of_encodings
1182  * array.
1183  * @return array list of encodings supported by php mbstring extension
1184  * @since 1.5.1 and 1.4.6
1185  */
1186 function sq_mb_list_encodings() {
1187
1188     // if it's already in the session, don't need to regenerate it
1189     if (sqgetGlobalVar('mb_supported_encodings',$mb_supported_encodings,SQ_SESSION)
1190      && is_array($mb_supported_encodings))
1191         return $mb_supported_encodings;
1192
1193     // check if mbstring extension is present
1194     if (! function_exists('mb_internal_encoding')) {
1195         $supported_encodings = array();
1196         sqsession_register($supported_encodings, 'mb_supported_encodings');
1197         return $supported_encodings;
1198     }
1199
1200     // php 5+ function
1201     if (function_exists('mb_list_encodings')) {
1202         $supported_encodings = mb_list_encodings();
1203         array_walk($supported_encodings, 'sq_lowercase_array_vals');
1204         sqsession_register($supported_encodings, 'mb_supported_encodings');
1205         return $supported_encodings;
1206     }
1207
1208     // save original encoding
1209     $orig_encoding=mb_internal_encoding();
1210
1211     $list_of_encoding=array(
1212         'pass',
1213         'auto',
1214         'ascii',
1215         'jis',
1216         'utf-8',
1217         'sjis',
1218         'euc-jp',
1219         'iso-8859-1',
1220         'iso-8859-2',
1221         'iso-8859-7',
1222         'iso-8859-9',
1223         'iso-8859-15',
1224         'koi8-r',
1225         'koi8-u',
1226         'big5',
1227         'gb2312',
1228         'gb18030',
1229         'windows-1251',
1230         'windows-1255',
1231         'windows-1256',
1232         'tis-620',
1233         'iso-2022-jp',
1234         'euc-cn',
1235         'euc-kr',
1236         'euc-tw',
1237         'uhc',
1238         'utf7-imap');
1239
1240     $supported_encodings=array();
1241
1242     foreach ($list_of_encoding as $encoding) {
1243         // try setting encodings. suppress warning messages
1244         if (@mb_internal_encoding($encoding))
1245             $supported_encodings[]=$encoding;
1246     }
1247
1248     // restore original encoding
1249     mb_internal_encoding($orig_encoding);
1250
1251     // register list in session
1252     sqsession_register($supported_encodings, 'mb_supported_encodings');
1253
1254     return $supported_encodings;
1255 }
1256
1257 /**
1258  * Callback function used to lowercase array values.
1259  * @param string $val array value
1260  * @param mixed $key array key
1261  * @since 1.5.1 and 1.4.6
1262  */
1263 function sq_lowercase_array_vals(&$val,$key) {
1264     $val = strtolower($val);
1265 }
1266
1267 /**
1268  * Callback function to trim whitespace from a value, to be used in array_walk
1269  * @param string $value value to trim
1270  * @since 1.5.2 and 1.4.7
1271  */
1272 function sq_trim_value ( &$value ) {
1273     $value = trim($value);
1274 }
1275
1276 /**
1277   * Gathers the list of secuirty tokens currently
1278   * stored in the user's preferences and optionally
1279   * purges old ones from the list.
1280   *
1281   * @param boolean $purge_old Indicates if old tokens
1282   *                           should be purged from the
1283   *                           list ("old" is 2 days or
1284   *                           older unless the administrator
1285   *                           overrides that value using
1286   *                           $max_token_age_days in
1287   *                           config/config_local.php)
1288   *                           (OPTIONAL; default is to always
1289   *                           purge old tokens)
1290   *
1291   * @return array The list of tokens
1292   *
1293   * @since 1.4.19 and 1.5.2
1294   *
1295   */
1296 function sm_get_user_security_tokens($purge_old=TRUE)
1297 {
1298
1299    global $data_dir, $username, $max_token_age_days;
1300
1301    $tokens = getPref($data_dir, $username, 'security_tokens', '');
1302    if (($tokens = unserialize($tokens)) === FALSE || !is_array($tokens))
1303       $tokens = array();
1304
1305    // purge old tokens if necessary
1306    //
1307    if ($purge_old)
1308    {
1309       if (empty($max_token_age_days)) $max_token_age_days = 2;
1310       $now = time();
1311       $discard_token_date = $now - ($max_token_age_days * 86400);
1312       $cleaned_tokens = array();
1313       foreach ($tokens as $token => $timestamp)
1314          if ($timestamp >= $discard_token_date)
1315             $cleaned_tokens[$token] = $timestamp;
1316       $tokens = $cleaned_tokens;
1317    }
1318
1319    return $tokens;
1320
1321 }
1322
1323 /**
1324   * Generates a security token that is then stored in
1325   * the user's preferences with a timestamp for later
1326   * verification/use.
1327   *
1328   * NOTE: The administrator can force SquirrelMail to generate
1329   * a new token every time one is requested (which may increase
1330   * obscurity through token randomness at the cost of some
1331   * performance) by adding the following to
1332   * config/config_local.php:   $do_not_use_single_token = TRUE;
1333   * Otherwise, only one token will be generated per user which
1334   * will change only after it expires or is used outside of the
1335   * validity period specified when calling sm_validate_security_token()
1336   *
1337   * WARNING: If the administrator has turned the token system
1338   *          off by setting $disable_security_tokens to TRUE in
1339   *          config/config.php or the configuration tool, this
1340   *          function will not store tokens in the user
1341   *          preferences (but it will still generate and return
1342   *          a random string).
1343   *
1344   * @param boolean $force_generate_new When TRUE, a new token will
1345   *                                    always be created even if current
1346   *                                    configuration dictates otherwise
1347   *                                    (OPTIONAL; default FALSE)
1348   *
1349   * @return string A security token
1350   *
1351   * @since 1.4.19 and 1.5.2
1352   *
1353   */
1354 function sm_generate_security_token($force_generate_new=FALSE)
1355 {
1356
1357    global $data_dir, $username, $disable_security_tokens, $do_not_use_single_token;
1358    $max_generation_tries = 1000;
1359
1360    $tokens = sm_get_user_security_tokens();
1361
1362    if (!$force_generate_new && !$do_not_use_single_token && !empty($tokens))
1363       return key($tokens);
1364
1365    $new_token = GenerateRandomString(12, '', 7);
1366    $count = 0;
1367    while (isset($tokens[$new_token]))
1368    {
1369       $new_token = GenerateRandomString(12, '', 7);
1370       if (++$count > $max_generation_tries)
1371       {
1372          logout_error(_("Fatal token generation error; please contact your system administrator or the SquirrelMail Team"));
1373          exit;
1374       }
1375    }
1376
1377    // is the token system enabled?  CAREFUL!
1378    //
1379    if (!$disable_security_tokens)
1380    {
1381       $tokens[$new_token] = time();
1382       setPref($data_dir, $username, 'security_tokens', serialize($tokens));
1383    }
1384
1385    return $new_token;
1386
1387 }
1388
1389 /**
1390   * Validates a given security token and optionally remove it
1391   * from the user's preferences if it was valid.  If the token
1392   * is too old but otherwise valid, it will still be rejected.
1393   *
1394   * "Too old" is 2 days or older unless the administrator
1395   * overrides that value using $max_token_age_days in
1396   * config/config_local.php
1397   *
1398   * WARNING: If the administrator has turned the token system
1399   *          off by setting $disable_security_tokens to TRUE in
1400   *          config/config.php or the configuration tool, this
1401   *          function will always return TRUE.
1402   *
1403   * @param string  $token           The token to validate
1404   * @param int     $validity_period The number of seconds tokens are valid
1405   *                                 for (set to zero to remove valid tokens
1406   *                                 after only one use; use 3600 to allow
1407   *                                 tokens to be reused for an hour)
1408   *                                 (OPTIONAL; default is to only allow tokens
1409   *                                 to be used once)
1410   *                                 NOTE this is unrelated to $max_token_age_days
1411   *                                 or rather is an additional time constraint on
1412   *                                 tokens that allows them to be re-used (or not)
1413   *                                 within a more narrow timeframe
1414   * @param boolean $show_error      Indicates that if the token is not
1415   *                                 valid, this function should display
1416   *                                 a generic error, log the user out
1417   *                                 and exit - this function will never
1418   *                                 return in that case.
1419   *                                 (OPTIONAL; default FALSE)
1420   *
1421   * @return boolean TRUE if the token validated; FALSE otherwise
1422   *
1423   * @since 1.4.19 and 1.5.2
1424   *
1425   */
1426 function sm_validate_security_token($token, $validity_period=0, $show_error=FALSE)
1427 {
1428
1429    global $data_dir, $username, $max_token_age_days,
1430           $disable_security_tokens;
1431
1432    // bypass token validation?  CAREFUL!
1433    //
1434    if ($disable_security_tokens) return TRUE;
1435
1436    // don't purge old tokens here because we already
1437    // do it when generating tokens
1438    //
1439    $tokens = sm_get_user_security_tokens(FALSE);
1440
1441    // token not found?
1442    //
1443    if (empty($tokens[$token]))
1444    {
1445       if (!$show_error) return FALSE;
1446       logout_error(_("This page request could not be verified and appears to have expired."));
1447       exit;
1448    }
1449
1450    $now = time();
1451    $timestamp = $tokens[$token];
1452
1453    // whether valid or not, we want to remove it from
1454    // user prefs if it's old enough
1455    //
1456    if ($timestamp < $now - $validity_period)
1457    {
1458       unset($tokens[$token]);
1459       setPref($data_dir, $username, 'security_tokens', serialize($tokens));
1460    }
1461
1462    // reject tokens that are too old
1463    //
1464    if (empty($max_token_age_days)) $max_token_age_days = 2;
1465    $old_token_date = $now - ($max_token_age_days * 86400);
1466    if ($timestamp < $old_token_date)
1467    {
1468       if (!$show_error) return FALSE;
1469       logout_error(_("The current page request appears to have originated from an untrusted source."));
1470       exit;
1471    }
1472
1473    // token OK!
1474    //
1475    return TRUE;
1476
1477 }
1478
1479 /**
1480   * Wrapper for PHP's htmlspecialchars() that
1481   * attempts to add the correct character encoding
1482   *
1483   * @param string $string The string to be converted
1484   * @param int $flags A bitmask that controls the behavior of htmlspecialchars()
1485   *                   (See http://php.net/manual/function.htmlspecialchars.php )
1486   *                   (OPTIONAL; default ENT_COMPAT)
1487   * @param string $encoding The character encoding to use in the conversion
1488   *                         (OPTIONAL; default automatic detection)
1489   * @param boolean $double_encode Whether or not to convert entities that are
1490   *                               already in the string (only supported in
1491   *                               PHP 5.2.3+) (OPTIONAL; default TRUE)
1492   *
1493   * @return string The converted text
1494   *
1495   */
1496 function sm_encode_html_special_chars($string, $flags=ENT_COMPAT,
1497                                       $encoding=NULL, $double_encode=TRUE)
1498 {
1499    if (!$encoding)
1500    {
1501       global $default_charset;
1502       if ($default_charset == 'iso-2022-jp')
1503          $default_charset = 'EUC-JP';
1504       $encoding = $default_charset;
1505    }
1506
1507    $string = iconv($encoding, 'UTF-8', $string);
1508    if (check_php_version(5, 2, 3))
1509       $ret = htmlspecialchars($string, $flags, 'UTF-8', $double_encode);
1510    else
1511       $ret = htmlspecialchars($string, $flags, 'UTF-8');
1512
1513    return iconv('UTF-8', $encoding, $ret);
1514 }
1515
1516 $PHP_SELF = php_self();