RoundCube Webmail
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1345 lines
43 KiB

10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
13 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
10 years ago
  1. <?php
  2. /**
  3. +-----------------------------------------------------------------------+
  4. | This file is part of the Roundcube Webmail client |
  5. | Copyright (C) 2008-2012, The Roundcube Dev Team |
  6. | Copyright (C) 2011-2012, Kolab Systems AG |
  7. | |
  8. | Licensed under the GNU General Public License version 3 or |
  9. | any later version with exceptions for skins & plugins. |
  10. | See the README file for a full license statement. |
  11. | |
  12. | PURPOSE: |
  13. | Utility class providing common functions |
  14. +-----------------------------------------------------------------------+
  15. | Author: Thomas Bruederli <roundcube@gmail.com> |
  16. | Author: Aleksander Machniak <alec@alec.pl> |
  17. +-----------------------------------------------------------------------+
  18. */
  19. /**
  20. * Utility class providing common functions
  21. *
  22. * @package Framework
  23. * @subpackage Utils
  24. */
  25. class rcube_utils
  26. {
  27. // define constants for input reading
  28. const INPUT_GET = 1;
  29. const INPUT_POST = 2;
  30. const INPUT_COOKIE = 4;
  31. const INPUT_GP = 3; // GET + POST
  32. const INPUT_GPC = 7; // GET + POST + COOKIE
  33. /**
  34. * Helper method to set a cookie with the current path and host settings
  35. *
  36. * @param string Cookie name
  37. * @param string Cookie value
  38. * @param string Expiration time
  39. */
  40. public static function setcookie($name, $value, $exp = 0)
  41. {
  42. if (headers_sent()) {
  43. return;
  44. }
  45. $cookie = session_get_cookie_params();
  46. $secure = $cookie['secure'] || self::https_check();
  47. setcookie($name, $value, $exp, $cookie['path'], $cookie['domain'], $secure, true);
  48. }
  49. /**
  50. * E-mail address validation.
  51. *
  52. * @param string $email Email address
  53. * @param boolean $dns_check True to check dns
  54. *
  55. * @return boolean True on success, False if address is invalid
  56. */
  57. public static function check_email($email, $dns_check=true)
  58. {
  59. // Check for invalid characters
  60. if (preg_match('/[\x00-\x1F\x7F-\xFF]/', $email)) {
  61. return false;
  62. }
  63. // Check for length limit specified by RFC 5321 (#1486453)
  64. if (strlen($email) > 254) {
  65. return false;
  66. }
  67. $email_array = explode('@', $email);
  68. // Check that there's one @ symbol
  69. if (count($email_array) < 2) {
  70. return false;
  71. }
  72. $domain_part = array_pop($email_array);
  73. $local_part = implode('@', $email_array);
  74. // from PEAR::Validate
  75. $regexp = '&^(?:
  76. ("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+")| #1 quoted name
  77. ([-\w!\#\$%\&\'*+~/^`|{}=]+(?:\.[-\w!\#\$%\&\'*+~/^`|{}=]+)*)) #2 OR dot-atom (RFC5322)
  78. $&xi';
  79. if (!preg_match($regexp, $local_part)) {
  80. return false;
  81. }
  82. // Validate domain part
  83. if (preg_match('/^\[((IPv6:[0-9a-f:.]+)|([0-9.]+))\]$/i', $domain_part, $matches)) {
  84. return self::check_ip(preg_replace('/^IPv6:/i', '', $matches[1])); // valid IPv4 or IPv6 address
  85. }
  86. else {
  87. // If not an IP address
  88. $domain_array = explode('.', $domain_part);
  89. // Not enough parts to be a valid domain
  90. if (count($domain_array) < 2) {
  91. return false;
  92. }
  93. foreach ($domain_array as $part) {
  94. if (!preg_match('/^((xn--)?([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part)) {
  95. return false;
  96. }
  97. }
  98. // last domain part
  99. $last_part = array_pop($domain_array);
  100. if (strpos($last_part, 'xn--') !== 0 && preg_match('/[^a-zA-Z]/', $last_part)) {
  101. return false;
  102. }
  103. $rcube = rcube::get_instance();
  104. if (!$dns_check || !function_exists('checkdnsrr') || !$rcube->config->get('email_dns_check')) {
  105. return true;
  106. }
  107. // Check DNS record(s)
  108. // Note: We can't use ANY (#6581)
  109. foreach (array('A', 'MX', 'CNAME', 'AAAA') as $type) {
  110. if (checkdnsrr($domain_part, $type)) {
  111. return true;
  112. }
  113. }
  114. }
  115. return false;
  116. }
  117. /**
  118. * Validates IPv4 or IPv6 address
  119. *
  120. * @param string $ip IP address in v4 or v6 format
  121. *
  122. * @return bool True if the address is valid
  123. */
  124. public static function check_ip($ip)
  125. {
  126. return filter_var($ip, FILTER_VALIDATE_IP) !== false;
  127. }
  128. /**
  129. * Check whether the HTTP referer matches the current request
  130. *
  131. * @return boolean True if referer is the same host+path, false if not
  132. */
  133. public static function check_referer()
  134. {
  135. $uri = parse_url($_SERVER['REQUEST_URI']);
  136. $referer = parse_url(self::request_header('Referer'));
  137. return $referer['host'] == self::request_header('Host') && $referer['path'] == $uri['path'];
  138. }
  139. /**
  140. * Replacing specials characters to a specific encoding type
  141. *
  142. * @param string Input string
  143. * @param string Encoding type: text|html|xml|js|url
  144. * @param string Replace mode for tags: show|remove|strict
  145. * @param boolean Convert newlines
  146. *
  147. * @return string The quoted string
  148. */
  149. public static function rep_specialchars_output($str, $enctype = '', $mode = '', $newlines = true)
  150. {
  151. static $html_encode_arr = false;
  152. static $js_rep_table = false;
  153. static $xml_rep_table = false;
  154. if (!is_string($str)) {
  155. $str = strval($str);
  156. }
  157. // encode for HTML output
  158. if ($enctype == 'html') {
  159. if (!$html_encode_arr) {
  160. $html_encode_arr = get_html_translation_table(HTML_SPECIALCHARS);
  161. unset($html_encode_arr['?']);
  162. }
  163. $encode_arr = $html_encode_arr;
  164. if ($mode == 'remove') {
  165. $str = strip_tags($str);
  166. }
  167. else if ($mode != 'strict') {
  168. // don't replace quotes and html tags
  169. $ltpos = strpos($str, '<');
  170. if ($ltpos !== false && strpos($str, '>', $ltpos) !== false) {
  171. unset($encode_arr['"']);
  172. unset($encode_arr['<']);
  173. unset($encode_arr['>']);
  174. unset($encode_arr['&']);
  175. }
  176. }
  177. $out = strtr($str, $encode_arr);
  178. return $newlines ? nl2br($out) : $out;
  179. }
  180. // if the replace tables for XML and JS are not yet defined
  181. if ($js_rep_table === false) {
  182. $js_rep_table = $xml_rep_table = array();
  183. $xml_rep_table['&'] = '&amp;';
  184. // can be increased to support more charsets
  185. for ($c=160; $c<256; $c++) {
  186. $xml_rep_table[chr($c)] = "&#$c;";
  187. }
  188. $xml_rep_table['"'] = '&quot;';
  189. $js_rep_table['"'] = '\\"';
  190. $js_rep_table["'"] = "\\'";
  191. $js_rep_table["\\"] = "\\\\";
  192. // Unicode line and paragraph separators (#1486310)
  193. $js_rep_table[chr(hexdec('E2')).chr(hexdec('80')).chr(hexdec('A8'))] = '&#8232;';
  194. $js_rep_table[chr(hexdec('E2')).chr(hexdec('80')).chr(hexdec('A9'))] = '&#8233;';
  195. }
  196. // encode for javascript use
  197. if ($enctype == 'js') {
  198. return preg_replace(array("/\r?\n/", "/\r/", '/<\\//'), array('\n', '\n', '<\\/'), strtr($str, $js_rep_table));
  199. }
  200. // encode for plaintext
  201. if ($enctype == 'text') {
  202. return str_replace("\r\n", "\n", $mode == 'remove' ? strip_tags($str) : $str);
  203. }
  204. if ($enctype == 'url') {
  205. return rawurlencode($str);
  206. }
  207. // encode for XML
  208. if ($enctype == 'xml') {
  209. return strtr($str, $xml_rep_table);
  210. }
  211. // no encoding given -> return original string
  212. return $str;
  213. }
  214. /**
  215. * Read input value and convert it for internal use
  216. * Performs stripslashes() and charset conversion if necessary
  217. *
  218. * @param string Field name to read
  219. * @param int Source to get value from (see self::INPUT_*)
  220. * @param boolean Allow HTML tags in field value
  221. * @param string Charset to convert into
  222. *
  223. * @return string Field value or NULL if not available
  224. */
  225. public static function get_input_value($fname, $source, $allow_html = false, $charset = null)
  226. {
  227. $value = null;
  228. if (($source & self::INPUT_GET) && isset($_GET[$fname])) {
  229. $value = $_GET[$fname];
  230. }
  231. if (($source & self::INPUT_POST) && isset($_POST[$fname])) {
  232. $value = $_POST[$fname];
  233. }
  234. if (($source & self::INPUT_COOKIE) && isset($_COOKIE[$fname])) {
  235. $value = $_COOKIE[$fname];
  236. }
  237. return self::parse_input_value($value, $allow_html, $charset);
  238. }
  239. /**
  240. * Parse/validate input value. See self::get_input_value()
  241. * Performs stripslashes() and charset conversion if necessary
  242. *
  243. * @param string Input value
  244. * @param boolean Allow HTML tags in field value
  245. * @param string Charset to convert into
  246. *
  247. * @return string Parsed value
  248. */
  249. public static function parse_input_value($value, $allow_html = false, $charset = null)
  250. {
  251. global $OUTPUT;
  252. if (empty($value)) {
  253. return $value;
  254. }
  255. if (is_array($value)) {
  256. foreach ($value as $idx => $val) {
  257. $value[$idx] = self::parse_input_value($val, $allow_html, $charset);
  258. }
  259. return $value;
  260. }
  261. // remove HTML tags if not allowed
  262. if (!$allow_html) {
  263. $value = strip_tags($value);
  264. }
  265. $output_charset = is_object($OUTPUT) ? $OUTPUT->get_charset() : null;
  266. // remove invalid characters (#1488124)
  267. if ($output_charset == 'UTF-8') {
  268. $value = rcube_charset::clean($value);
  269. }
  270. // convert to internal charset
  271. if ($charset && $output_charset) {
  272. $value = rcube_charset::convert($value, $output_charset, $charset);
  273. }
  274. return $value;
  275. }
  276. /**
  277. * Convert array of request parameters (prefixed with _)
  278. * to a regular array with non-prefixed keys.
  279. *
  280. * @param int $mode Source to get value from (GPC)
  281. * @param string $ignore PCRE expression to skip parameters by name
  282. * @param boolean $allow_html Allow HTML tags in field value
  283. *
  284. * @return array Hash array with all request parameters
  285. */
  286. public static function request2param($mode = null, $ignore = 'task|action', $allow_html = false)
  287. {
  288. $out = array();
  289. $src = $mode == self::INPUT_GET ? $_GET : ($mode == self::INPUT_POST ? $_POST : $_REQUEST);
  290. foreach (array_keys($src) as $key) {
  291. $fname = $key[0] == '_' ? substr($key, 1) : $key;
  292. if ($ignore && !preg_match('/^(' . $ignore . ')$/', $fname)) {
  293. $out[$fname] = self::get_input_value($key, $mode, $allow_html);
  294. }
  295. }
  296. return $out;
  297. }
  298. /**
  299. * Convert the given string into a valid HTML identifier
  300. * Same functionality as done in app.js with rcube_webmail.html_identifier()
  301. */
  302. public static function html_identifier($str, $encode=false)
  303. {
  304. if ($encode) {
  305. return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
  306. }
  307. else {
  308. return asciiwords($str, true, '_');
  309. }
  310. }
  311. /**
  312. * Replace all css definitions with #container [def]
  313. * and remove css-inlined scripting, make position style safe
  314. *
  315. * @param string CSS source code
  316. * @param string Container ID to use as prefix
  317. * @param bool Allow remote content
  318. *
  319. * @return string Modified CSS source
  320. */
  321. public static function mod_css_styles($source, $container_id, $allow_remote = false)
  322. {
  323. $last_pos = 0;
  324. $replacements = new rcube_string_replacer;
  325. // ignore the whole block if evil styles are detected
  326. $source = self::xss_entity_decode($source);
  327. $stripped = preg_replace('/[^a-z\(:;]/i', '', $source);
  328. $evilexpr = 'expression|behavior|javascript:|import[^a]' . (!$allow_remote ? '|url\((?!data:image)' : '');
  329. if (preg_match("/$evilexpr/i", $stripped)) {
  330. return '/* evil! */';
  331. }
  332. $strict_url_regexp = '!url\s*\(\s*["\']?(https?:)//[a-z0-9/._+-]+["\']?\s*\)!Uims';
  333. // cut out all contents between { and }
  334. while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) {
  335. $nested = strpos($source, '{', $pos+1);
  336. if ($nested && $nested < $pos2) // when dealing with nested blocks (e.g. @media), take the inner one
  337. $pos = $nested;
  338. $length = $pos2 - $pos - 1;
  339. $styles = substr($source, $pos+1, $length);
  340. // Convert position:fixed to position:absolute (#5264)
  341. $styles = preg_replace('/position[^a-z]*:[\s\r\n]*fixed/i', 'position: absolute', $styles);
  342. // check every line of a style block...
  343. if ($allow_remote) {
  344. $a_styles = preg_split('/;[\r\n]*/', $styles, -1, PREG_SPLIT_NO_EMPTY);
  345. for ($i=0, $len=count($a_styles); $i < $len; $i++) {
  346. $line = $a_styles[$i];
  347. $stripped = preg_replace('/[^a-z\(:;]/i', '', $line);
  348. // allow data:image uri, join with continuation
  349. if (stripos($stripped, 'url(data:image')) {
  350. $a_styles[$i] .= ';' . $a_styles[$i+1];
  351. unset($a_styles[$i+1]);
  352. }
  353. // allow strict url() values only
  354. else if (stripos($stripped, 'url(') && !preg_match($strict_url_regexp, $line)) {
  355. $a_styles = array('/* evil! */');
  356. break;
  357. }
  358. }
  359. $styles = join(";\n", $a_styles);
  360. }
  361. $key = $replacements->add($styles);
  362. $repl = $replacements->get_replacement($key);
  363. $source = substr_replace($source, $repl, $pos+1, $length);
  364. $last_pos = $pos2 - ($length - strlen($repl));
  365. }
  366. /*
  367. // remove html comments and add #container to each tag selector.
  368. // also replace body definition because we also stripped off the <body> tag
  369. $source = preg_replace(
  370. array(
  371. '/(^\s*<\!--)|(-->\s*$)/m',
  372. // (?!##str) below is to not match with ##str_replacement_0##
  373. // from rcube_string_replacer used above, this is needed for
  374. // cases like @media { body { position: fixed; } } (#5811)
  375. '/(^\s*|,\s*|\}\s*|\{\s*)((?!##str)[a-z0-9\._#\*][a-z0-9\.\-_]*)/im',
  376. '/'.preg_quote($container_id, '/').'\s+body/i',
  377. ),
  378. array(
  379. '',
  380. "\\1#$container_id \\2",
  381. $container_id,
  382. ),
  383. $source);
  384. */
  385. // remove html comments
  386. $source = preg_replace('/(^\s*<\!--)|(-->\s*$)/m', '', $source);
  387. // add #container to each tag selector and prefix to id/class identifiers
  388. if ($container_id) {
  389. // Exclude rcube_string_replacer pattern matches, this is needed
  390. // for cases like @media { body { position: fixed; } } (#5811)
  391. $excl = '(?!' . substr($replacements->pattern, 1, -1) . ')';
  392. $regexp = '/(^\s*|,\s*|\}\s*|\{\s*)(' . $excl . ':?[a-z0-9\._#\*\[][a-z0-9\._:\(\)#=~ \[\]"\|\>\+\$\^-]*)/im';
  393. $callback = function($matches) use ($container_id, $prefix) {
  394. $replace = $matches[2];
  395. if (stripos($replace, ':root') === 0) {
  396. $replace = substr($replace, 5);
  397. }
  398. $replace = "#$container_id " . $replace;
  399. // Remove redundant spaces (for simpler testing)
  400. $replace = preg_replace('/\s+/', ' ', $replace);
  401. return str_replace($matches[2], $replace, $matches[0]);
  402. };
  403. $source = preg_replace_callback($regexp, $callback, $source);
  404. }
  405. // replace body definition because we also stripped off the <body> tag
  406. if ($container_id) {
  407. $regexp = '/#' . preg_quote($container_id, '/') . '\s+body/i';
  408. $source = preg_replace($regexp, "#$container_id", $source);
  409. }
  410. // put block contents back in
  411. $source = $replacements->resolve($source);
  412. return $source;
  413. }
  414. /**
  415. * Generate CSS classes from mimetype and filename extension
  416. *
  417. * @param string $mimetype Mimetype
  418. * @param string $filename Filename
  419. *
  420. * @return string CSS classes separated by space
  421. */
  422. public static function file2class($mimetype, $filename)
  423. {
  424. $mimetype = strtolower($mimetype);
  425. $filename = strtolower($filename);
  426. list($primary, $secondary) = explode('/', $mimetype);
  427. $classes = array($primary ?: 'unknown');
  428. if ($secondary) {
  429. $classes[] = $secondary;
  430. }
  431. if (preg_match('/\.([a-z0-9]+)$/', $filename, $m)) {
  432. if (!in_array($m[1], $classes)) {
  433. $classes[] = $m[1];
  434. }
  435. }
  436. return join(" ", $classes);
  437. }
  438. /**
  439. * Decode escaped entities used by known XSS exploits.
  440. * See http://downloads.securityfocus.com/vulnerabilities/exploits/26800.eml for examples
  441. *
  442. * @param string CSS content to decode
  443. *
  444. * @return string Decoded string
  445. */
  446. public static function xss_entity_decode($content)
  447. {
  448. $callback = function($matches) { return chr(hexdec($matches[1])); };
  449. $out = html_entity_decode(html_entity_decode($content));
  450. $out = trim(preg_replace('/(^<!--|-->$)/', '', trim($out)));
  451. $out = preg_replace_callback('/\\\([0-9a-f]{2,6})\s*/i', $callback, $out);
  452. $out = preg_replace('/\\\([^0-9a-f])/i', '\\1', $out);
  453. $out = preg_replace('#/\*.*\*/#Ums', '', $out);
  454. $out = strip_tags($out);
  455. return $out;
  456. }
  457. /**
  458. * Check if we can process not exceeding memory_limit
  459. *
  460. * @param integer Required amount of memory
  461. *
  462. * @return boolean True if memory won't be exceeded, False otherwise
  463. */
  464. public static function mem_check($need)
  465. {
  466. $mem_limit = parse_bytes(ini_get('memory_limit'));
  467. $memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB
  468. return $mem_limit > 0 && $memory + $need > $mem_limit ? false : true;
  469. }
  470. /**
  471. * Check if working in SSL mode
  472. *
  473. * @param integer $port HTTPS port number
  474. * @param boolean $use_https Enables 'use_https' option checking
  475. *
  476. * @return boolean
  477. */
  478. public static function https_check($port=null, $use_https=true)
  479. {
  480. if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') {
  481. return true;
  482. }
  483. if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])
  484. && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https'
  485. && in_array($_SERVER['REMOTE_ADDR'], rcube::get_instance()->config->get('proxy_whitelist', array()))
  486. ) {
  487. return true;
  488. }
  489. if ($port && $_SERVER['SERVER_PORT'] == $port) {
  490. return true;
  491. }
  492. if ($use_https && rcube::get_instance()->config->get('use_https')) {
  493. return true;
  494. }
  495. return false;
  496. }
  497. /**
  498. * Replaces hostname variables.
  499. *
  500. * @param string $name Hostname
  501. * @param string $host Optional IMAP hostname
  502. *
  503. * @return string Hostname
  504. */
  505. public static function parse_host($name, $host = '')
  506. {
  507. if (!is_string($name)) {
  508. return $name;
  509. }
  510. // %n - host
  511. $n = preg_replace('/:\d+$/', '', $_SERVER['SERVER_NAME']);
  512. // %t - host name without first part, e.g. %n=mail.domain.tld, %t=domain.tld
  513. // If %n=domain.tld then %t=domain.tld as well (remains valid)
  514. $t = preg_replace('/^[^.]+\.(?![^.]+$)/', '', $n);
  515. // %d - domain name without first part (up to domain.tld)
  516. $d = preg_replace('/^[^.]+\.(?![^.]+$)/', '', $_SERVER['HTTP_HOST']);
  517. // %h - IMAP host
  518. $h = $_SESSION['storage_host'] ?: $host;
  519. // %z - IMAP domain without first part, e.g. %h=imap.domain.tld, %z=domain.tld
  520. // If %h=domain.tld then %z=domain.tld as well (remains valid)
  521. $z = preg_replace('/^[^.]+\.(?![^.]+$)/', '', $h);
  522. // %s - domain name after the '@' from e-mail address provided at login screen.
  523. // Returns FALSE if an invalid email is provided
  524. if (strpos($name, '%s') !== false) {
  525. $user_email = self::get_input_value('_user', self::INPUT_POST);
  526. $user_email = self::idn_convert($user_email, true);
  527. $matches = preg_match('/(.*)@([a-z0-9\.\-\[\]\:]+)/i', $user_email, $s);
  528. if ($matches < 1 || filter_var($s[1]."@".$s[2], FILTER_VALIDATE_EMAIL) === false) {
  529. return false;
  530. }
  531. }
  532. return str_replace(array('%n', '%t', '%d', '%h', '%z', '%s'), array($n, $t, $d, $h, $z, $s[2]), $name);
  533. }
  534. /**
  535. * Returns remote IP address and forwarded addresses if found
  536. *
  537. * @return string Remote IP address(es)
  538. */
  539. public static function remote_ip()
  540. {
  541. $address = $_SERVER['REMOTE_ADDR'];
  542. // append the NGINX X-Real-IP header, if set
  543. if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
  544. $remote_ip[] = 'X-Real-IP: ' . $_SERVER['HTTP_X_REAL_IP'];
  545. }
  546. // append the X-Forwarded-For header, if set
  547. if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  548. $remote_ip[] = 'X-Forwarded-For: ' . $_SERVER['HTTP_X_FORWARDED_FOR'];
  549. }
  550. if (!empty($remote_ip)) {
  551. $address .= '(' . implode(',', $remote_ip) . ')';
  552. }
  553. return $address;
  554. }
  555. /**
  556. * Returns the real remote IP address
  557. *
  558. * @return string Remote IP address
  559. */
  560. public static function remote_addr()
  561. {
  562. // Check if any of the headers are set first to improve performance
  563. if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']) || !empty($_SERVER['HTTP_X_REAL_IP'])) {
  564. $proxy_whitelist = rcube::get_instance()->config->get('proxy_whitelist', array());
  565. if (in_array($_SERVER['REMOTE_ADDR'], $proxy_whitelist)) {
  566. if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  567. foreach (array_reverse(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])) as $forwarded_ip) {
  568. if (!in_array($forwarded_ip, $proxy_whitelist)) {
  569. return $forwarded_ip;
  570. }
  571. }
  572. }
  573. if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
  574. return $_SERVER['HTTP_X_REAL_IP'];
  575. }
  576. }
  577. }
  578. if (!empty($_SERVER['REMOTE_ADDR'])) {
  579. return $_SERVER['REMOTE_ADDR'];
  580. }
  581. return '';
  582. }
  583. /**
  584. * Read a specific HTTP request header.
  585. *
  586. * @param string $name Header name
  587. *
  588. * @return mixed Header value or null if not available
  589. */
  590. public static function request_header($name)
  591. {
  592. if (function_exists('getallheaders')) {
  593. $hdrs = array_change_key_case(getallheaders(), CASE_UPPER);
  594. $key = strtoupper($name);
  595. }
  596. else {
  597. $key = 'HTTP_' . strtoupper(strtr($name, '-', '_'));
  598. $hdrs = array_change_key_case($_SERVER, CASE_UPPER);
  599. }
  600. return $hdrs[$key];
  601. }
  602. /**
  603. * Explode quoted string
  604. *
  605. * @param string Delimiter expression string for preg_match()
  606. * @param string Input string
  607. *
  608. * @return array String items
  609. */
  610. public static function explode_quoted_string($delimiter, $string)
  611. {
  612. $result = array();
  613. $strlen = strlen($string);
  614. for ($q=$p=$i=0; $i < $strlen; $i++) {
  615. if ($string[$i] == "\"" && $string[$i-1] != "\\") {
  616. $q = $q ? false : true;
  617. }
  618. else if (!$q && preg_match("/$delimiter/", $string[$i])) {
  619. $result[] = substr($string, $p, $i - $p);
  620. $p = $i + 1;
  621. }
  622. }
  623. $result[] = (string) substr($string, $p);
  624. return $result;
  625. }
  626. /**
  627. * Improved equivalent to strtotime()
  628. *
  629. * @param string $date Date string
  630. * @param DateTimeZone $timezone Timezone to use for DateTime object
  631. *
  632. * @return int Unix timestamp
  633. */
  634. public static function strtotime($date, $timezone = null)
  635. {
  636. $date = self::clean_datestr($date);
  637. $tzname = $timezone ? ' ' . $timezone->getName() : '';
  638. // unix timestamp
  639. if (is_numeric($date)) {
  640. return (int) $date;
  641. }
  642. // It can be very slow when provided string is not a date and very long
  643. if (strlen($date) > 128) {
  644. $date = substr($date, 0, 128);
  645. }
  646. // if date parsing fails, we have a date in non-rfc format.
  647. // remove token from the end and try again
  648. while (($ts = @strtotime($date . $tzname)) === false || $ts < 0) {
  649. if (($pos = strrpos($date, ' ')) === false) {
  650. break;
  651. }
  652. $date = rtrim(substr($date, 0, $pos));
  653. }
  654. return (int) $ts;
  655. }
  656. /**
  657. * Date parsing function that turns the given value into a DateTime object
  658. *
  659. * @param string $date Date string
  660. * @param DateTimeZone $timezone Timezone to use for DateTime object
  661. *
  662. * @return DateTime instance or false on failure
  663. */
  664. public static function anytodatetime($date, $timezone = null)
  665. {
  666. if ($date instanceof DateTime) {
  667. return $date;
  668. }
  669. $dt = false;
  670. $date = self::clean_datestr($date);
  671. // try to parse string with DateTime first
  672. if (!empty($date)) {
  673. try {
  674. $_date = preg_match('/^[0-9]+$/', $date) ? "@$date" : $date;
  675. $dt = $timezone ? new DateTime($_date, $timezone) : new DateTime($_date);
  676. }
  677. catch (Exception $e) {
  678. // ignore
  679. }
  680. }
  681. // try our advanced strtotime() method
  682. if (!$dt && ($timestamp = self::strtotime($date, $timezone))) {
  683. try {
  684. $dt = new DateTime("@".$timestamp);
  685. if ($timezone) {
  686. $dt->setTimezone($timezone);
  687. }
  688. }
  689. catch (Exception $e) {
  690. // ignore
  691. }
  692. }
  693. return $dt;
  694. }
  695. /**
  696. * Clean up date string for strtotime() input
  697. *
  698. * @param string $date Date string
  699. *
  700. * @return string Date string
  701. */
  702. public static function clean_datestr($date)
  703. {
  704. $date = trim($date);
  705. // check for MS Outlook vCard date format YYYYMMDD
  706. if (preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $date, $m)) {
  707. return sprintf('%04d-%02d-%02d 00:00:00', intval($m[1]), intval($m[2]), intval($m[3]));
  708. }
  709. // Clean malformed data
  710. $date = preg_replace(
  711. array(
  712. '/\(.*\)/', // remove RFC comments
  713. '/GMT\s*([+-][0-9]+)/', // support non-standard "GMTXXXX" literal
  714. '/[^a-z0-9\x20\x09:\/\.+-]/i', // remove any invalid characters
  715. '/\s*(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s*/i', // remove weekday names
  716. ),
  717. array(
  718. '',
  719. '\\1',
  720. '',
  721. '',
  722. ), $date);
  723. $date = trim($date);
  724. // try to fix dd/mm vs. mm/dd discrepancy, we can't do more here
  725. if (preg_match('/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})(\s.*)?$/', $date, $m)) {
  726. $mdy = $m[2] > 12 && $m[1] <= 12;
  727. $day = $mdy ? $m[2] : $m[1];
  728. $month = $mdy ? $m[1] : $m[2];
  729. $date = sprintf('%04d-%02d-%02d%s', $m[3], $month, $day, $m[4] ?: ' 00:00:00');
  730. }
  731. // I've found that YYYY.MM.DD is recognized wrong, so here's a fix
  732. else if (preg_match('/^(\d{4})\.(\d{1,2})\.(\d{1,2})(\s.*)?$/', $date, $m)) {
  733. $date = sprintf('%04d-%02d-%02d%s', $m[1], $m[2], $m[3], $m[4] ?: ' 00:00:00');
  734. }
  735. return $date;
  736. }
  737. /**
  738. * Turns the given date-only string in defined format into YYYY-MM-DD format.
  739. *
  740. * Supported formats: 'Y/m/d', 'Y.m.d', 'd-m-Y', 'd/m/Y', 'd.m.Y', 'j.n.Y'
  741. *
  742. * @param string $date Date string
  743. * @param string $format Input date format
  744. *
  745. * @return strin Date string in YYYY-MM-DD format, or the original string
  746. * if format is not supported
  747. */
  748. public static function format_datestr($date, $format)
  749. {
  750. $format_items = preg_split('/[.-\/\\\\]/', $format);
  751. $date_items = preg_split('/[.-\/\\\\]/', $date);
  752. $iso_format = '%04d-%02d-%02d';
  753. if (count($format_items) == 3 && count($date_items) == 3) {
  754. if ($format_items[0] == 'Y') {
  755. $date = sprintf($iso_format, $date_items[0], $date_items[1], $date_items[2]);
  756. }
  757. else if (strpos('dj', $format_items[0]) !== false) {
  758. $date = sprintf($iso_format, $date_items[2], $date_items[1], $date_items[0]);
  759. }
  760. else if (strpos('mn', $format_items[0]) !== false) {
  761. $date = sprintf($iso_format, $date_items[2], $date_items[0], $date_items[1]);
  762. }
  763. }
  764. return $date;
  765. }
  766. /**
  767. * Wrapper for idn_to_ascii with support for e-mail address.
  768. *
  769. * Warning: Domain names may be lowercase'd.
  770. * Warning: An empty string may be returned on invalid domain.
  771. *
  772. * @param string $str Decoded e-mail address
  773. *
  774. * @return string Encoded e-mail address
  775. */
  776. public static function idn_to_ascii($str)
  777. {
  778. return self::idn_convert($str, true);
  779. }
  780. /**
  781. * Wrapper for idn_to_utf8 with support for e-mail address
  782. *
  783. * @param string $str Decoded e-mail address
  784. *
  785. * @return string Encoded e-mail address
  786. */
  787. public static function idn_to_utf8($str)
  788. {
  789. return self::idn_convert($str, false);
  790. }
  791. /**
  792. * Convert a string to ascii or utf8 (using IDNA standard)
  793. *
  794. * @param string $input Decoded e-mail address
  795. * @param boolean $is_utf Convert by idn_to_ascii if true and idn_to_utf8 if false
  796. *
  797. * @return string Encoded e-mail address
  798. */
  799. public static function idn_convert($input, $is_utf = false)
  800. {
  801. if ($at = strpos($input, '@')) {
  802. $user = substr($input, 0, $at);
  803. $domain = substr($input, $at + 1);
  804. }
  805. else {
  806. $user = '';
  807. $domain = $input;
  808. }
  809. // Note that in PHP 7.2/7.3 calling idn_to_* functions with default arguments
  810. // throws a warning, so we have to set the variant explicitely (#6075)
  811. $variant = defined('INTL_IDNA_VARIANT_UTS46') ? INTL_IDNA_VARIANT_UTS46 : null;
  812. $options = 0;
  813. // Because php-intl extension lowercases domains and return false
  814. // on invalid input (#6224), we skip conversion when not needed
  815. // for compatibility with our Net_IDNA2 wrappers in bootstrap.php
  816. if ($is_utf) {
  817. if (preg_match('/[^\x20-\x7E]/', $domain)) {
  818. $domain = idn_to_ascii($domain, $options, $variant);
  819. }
  820. }
  821. else if (preg_match('/(^|\.)xn--/i', $domain)) {
  822. $domain = idn_to_utf8($domain, $options, $variant);
  823. }
  824. if ($domain === false) {
  825. return '';
  826. }
  827. return $at ? $user . '@' . $domain : $domain;
  828. }
  829. /**
  830. * Split the given string into word tokens
  831. *
  832. * @param string Input to tokenize
  833. * @param integer Minimum length of a single token
  834. * @return array List of tokens
  835. */
  836. public static function tokenize_string($str, $minlen = 2)
  837. {
  838. $expr = array('/[\s;,"\'\/+-]+/ui', '/(\d)[-.\s]+(\d)/u');
  839. $repl = array(' ', '\\1\\2');
  840. if ($minlen > 1) {
  841. $minlen--;
  842. $expr[] = "/(^|\s+)\w{1,$minlen}(\s+|$)/u";
  843. $repl[] = ' ';
  844. }
  845. return array_filter(explode(" ", preg_replace($expr, $repl, $str)));
  846. }
  847. /**
  848. * Normalize the given string for fulltext search.
  849. * Currently only optimized for ISO-8859-1 and ISO-8859-2 characters; to be extended
  850. *
  851. * @param string Input string (UTF-8)
  852. * @param boolean True to return list of words as array
  853. * @param integer Minimum length of tokens
  854. *
  855. * @return mixed Normalized string or a list of normalized tokens
  856. */
  857. public static function normalize_string($str, $as_array = false, $minlen = 2)
  858. {
  859. // replace 4-byte unicode characters with '?' character,
  860. // these are not supported in default utf-8 charset on mysql,
  861. // the chance we'd need them in searching is very low
  862. $str = preg_replace('/('
  863. . '\xF0[\x90-\xBF][\x80-\xBF]{2}'
  864. . '|[\xF1-\xF3][\x80-\xBF]{3}'
  865. . '|\xF4[\x80-\x8F][\x80-\xBF]{2}'
  866. . ')/', '?', $str);
  867. // split by words
  868. $arr = self::tokenize_string($str, $minlen);
  869. // detect character set
  870. if (utf8_encode(utf8_decode($str)) == $str) {
  871. // ISO-8859-1 (or ASCII)
  872. preg_match_all('/./u', 'äâàåáãæçéêëèïîìíñöôòøõóüûùúýÿ', $keys);
  873. preg_match_all('/./', 'aaaaaaaceeeeiiiinoooooouuuuyy', $values);
  874. $mapping = array_combine($keys[0], $values[0]);
  875. $mapping = array_merge($mapping, array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u'));
  876. }
  877. else if (rcube_charset::convert(rcube_charset::convert($str, 'UTF-8', 'ISO-8859-2'), 'ISO-8859-2', 'UTF-8') == $str) {
  878. // ISO-8859-2
  879. preg_match_all('/./u', 'ąáâäćçčéęëěíîłľĺńňóôöŕřśšşťţůúűüźžżý', $keys);
  880. preg_match_all('/./', 'aaaaccceeeeiilllnnooorrsssttuuuuzzzy', $values);
  881. $mapping = array_combine($keys[0], $values[0]);
  882. $mapping = array_merge($mapping, array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u'));
  883. }
  884. foreach ($arr as $i => $part) {
  885. $part = mb_strtolower($part);
  886. if (!empty($mapping)) {
  887. $part = strtr($part, $mapping);
  888. }
  889. $arr[$i] = $part;
  890. }
  891. return $as_array ? $arr : join(" ", $arr);
  892. }
  893. /**
  894. * Compare two strings for matching words (order not relevant)
  895. *
  896. * @param string Haystack
  897. * @param string Needle
  898. *
  899. * @return boolean True if match, False otherwise
  900. */
  901. public static function words_match($haystack, $needle)
  902. {
  903. $a_needle = self::tokenize_string($needle, 1);
  904. $_haystack = join(" ", self::tokenize_string($haystack, 1));
  905. $valid = strlen($_haystack) > 0;
  906. $hits = 0;
  907. foreach ($a_needle as $w) {
  908. if ($valid) {
  909. if (stripos($_haystack, $w) !== false) {
  910. $hits++;
  911. }
  912. }
  913. else if (stripos($haystack, $w) !== false) {
  914. $hits++;
  915. }
  916. }
  917. return $hits >= count($a_needle);
  918. }
  919. /**
  920. * Parse commandline arguments into a hash array
  921. *
  922. * @param array $aliases Argument alias names
  923. *
  924. * @return array Argument values hash
  925. */
  926. public static function get_opt($aliases = array())
  927. {
  928. $args = array();
  929. $bool = array();
  930. // find boolean (no value) options
  931. foreach ($aliases as $key => $alias) {
  932. if ($pos = strpos($alias, ':')) {
  933. $aliases[$key] = substr($alias, 0, $pos);
  934. $bool[] = $key;
  935. $bool[] = $aliases[$key];
  936. }
  937. }
  938. for ($i=1; $i < count($_SERVER['argv']); $i++) {
  939. $arg = $_SERVER['argv'][$i];
  940. $value = true;
  941. $key = null;
  942. if ($arg[0] == '-') {
  943. $key = preg_replace('/^-+/', '', $arg);
  944. $sp = strpos($arg, '=');
  945. if ($sp > 0) {
  946. $key = substr($key, 0, $sp - 2);
  947. $value = substr($arg, $sp+1);
  948. }
  949. else if (in_array($key, $bool)) {
  950. $value = true;
  951. }
  952. else if (strlen($_SERVER['argv'][$i+1]) && $_SERVER['argv'][$i+1][0] != '-') {
  953. $value = $_SERVER['argv'][++$i];
  954. }
  955. $args[$key] = is_string($value) ? preg_replace(array('/^["\']/', '/["\']$/'), '', $value) : $value;
  956. }
  957. else {
  958. $args[] = $arg;
  959. }
  960. if ($alias = $aliases[$key]) {
  961. $args[$alias] = $args[$key];
  962. }
  963. }
  964. return $args;
  965. }
  966. /**
  967. * Safe password prompt for command line
  968. * from http://blogs.sitepoint.com/2009/05/01/interactive-cli-password-prompt-in-php/
  969. *
  970. * @return string Password
  971. */
  972. public static function prompt_silent($prompt = "Password:")
  973. {
  974. if (preg_match('/^win/i', PHP_OS)) {
  975. $vbscript = sys_get_temp_dir() . 'prompt_password.vbs';
  976. $vbcontent = 'wscript.echo(InputBox("' . addslashes($prompt) . '", "", "password here"))';
  977. file_put_contents($vbscript, $vbcontent);
  978. $command = "cscript //nologo " . escapeshellarg($vbscript);
  979. $password = rtrim(shell_exec($command));
  980. unlink($vbscript);
  981. return $password;
  982. }
  983. else {
  984. $command = "/usr/bin/env bash -c 'echo OK'";
  985. if (rtrim(shell_exec($command)) !== 'OK') {
  986. echo $prompt;
  987. $pass = trim(fgets(STDIN));
  988. echo chr(8)."\r" . $prompt . str_repeat("*", strlen($pass))."\n";
  989. return $pass;
  990. }
  991. $command = "/usr/bin/env bash -c 'read -s -p \"" . addslashes($prompt) . "\" mypassword && echo \$mypassword'";
  992. $password = rtrim(shell_exec($command));
  993. echo "\n";
  994. return $password;
  995. }
  996. }
  997. /**
  998. * Find out if the string content means true or false
  999. *
  1000. * @param string $str Input value
  1001. *
  1002. * @return boolean Boolean value
  1003. */
  1004. public static function get_boolean($str)
  1005. {
  1006. $str = strtolower($str);
  1007. return !in_array($str, array('false', '0', 'no', 'off', 'nein', ''), true);
  1008. }
  1009. /**
  1010. * OS-dependent absolute path detection
  1011. */
  1012. public static function is_absolute_path($path)
  1013. {
  1014. if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
  1015. return (bool) preg_match('!^[a-z]:[\\\\/]!i', $path);
  1016. }
  1017. else {
  1018. return $path[0] == '/';
  1019. }
  1020. }
  1021. /**
  1022. * Resolve relative URL
  1023. *
  1024. * @param string $url Relative URL
  1025. *
  1026. * @return string Absolute URL
  1027. */
  1028. public static function resolve_url($url)
  1029. {
  1030. // prepend protocol://hostname:port
  1031. if (!preg_match('|^https?://|', $url)) {
  1032. $schema = 'http';
  1033. $default_port = 80;
  1034. if (self::https_check()) {
  1035. $schema = 'https';
  1036. $default_port = 443;
  1037. }
  1038. $prefix = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);
  1039. if ($_SERVER['SERVER_PORT'] != $default_port) {
  1040. $prefix .= ':' . $_SERVER['SERVER_PORT'];
  1041. }
  1042. $url = $prefix . ($url[0] == '/' ? '' : '/') . $url;
  1043. }
  1044. return $url;
  1045. }
  1046. /**
  1047. * Generate a random string
  1048. *
  1049. * @param int $length String length
  1050. * @param bool $raw Return RAW data instead of ascii
  1051. *
  1052. * @return string The generated random string
  1053. */
  1054. public static function random_bytes($length, $raw = false)
  1055. {
  1056. $hextab = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  1057. $tabsize = strlen($hextab);
  1058. // Use PHP7 true random generator
  1059. if ($raw && function_exists('random_bytes')) {
  1060. return random_bytes($length);
  1061. }
  1062. if (!$raw && function_exists('random_int')) {
  1063. $result = '';
  1064. while ($length-- > 0) {
  1065. $result .= $hextab[random_int(0, $tabsize - 1)];
  1066. }
  1067. return $result;
  1068. }
  1069. $random = openssl_random_pseudo_bytes($length);
  1070. if ($random === false && $length > 0) {
  1071. throw new Exception("Failed to get random bytes");
  1072. }
  1073. if (!$raw) {
  1074. for ($x = 0; $x < $length; $x++) {
  1075. $random[$x] = $hextab[ord($random[$x]) % $tabsize];
  1076. }
  1077. }
  1078. return $random;
  1079. }
  1080. /**
  1081. * Convert binary data into readable form (containing a-zA-Z0-9 characters)
  1082. *
  1083. * @param string $input Binary input
  1084. *
  1085. * @return string Readable output (Base62)
  1086. * @deprecated since 1.3.1
  1087. */
  1088. public static function bin2ascii($input)
  1089. {
  1090. $hextab = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  1091. $result = '';
  1092. for ($x = 0; $x < strlen($input); $x++) {
  1093. $result .= $hextab[ord($input[$x]) % 62];
  1094. }
  1095. return $result;
  1096. }
  1097. /**
  1098. * Format current date according to specified format.
  1099. * This method supports microseconds (u).
  1100. *
  1101. * @param string $format Date format (default: 'd-M-Y H:i:s O')
  1102. *
  1103. * @return string Formatted date
  1104. */
  1105. public static function date_format($format = null)
  1106. {
  1107. if (empty($format)) {
  1108. $format = 'd-M-Y H:i:s O';
  1109. }
  1110. if (strpos($format, 'u') !== false) {
  1111. $dt = number_format(microtime(true), 6, '.', '');
  1112. $dt .= '.' . date_default_timezone_get();
  1113. if ($date = date_create_from_format('U.u.e', $dt)) {
  1114. return $date->format($format);
  1115. }
  1116. }
  1117. return date($format);
  1118. }
  1119. /**
  1120. * Parses socket options and returns options for specified hostname.
  1121. *
  1122. * @param array &$options Configured socket options
  1123. * @param string $host Hostname
  1124. */
  1125. public static function parse_socket_options(&$options, $host = null)
  1126. {
  1127. if (empty($host) || empty($options)) {
  1128. return $options;
  1129. }
  1130. // get rid of schema and port from the hostname
  1131. $host_url = parse_url($host);
  1132. if (isset($host_url['host'])) {
  1133. $host = $host_url['host'];
  1134. }
  1135. // find per-host options
  1136. if (array_key_exists($host, $options)) {
  1137. $options = $options[$host];
  1138. }
  1139. }
  1140. /**
  1141. * Get maximum upload size
  1142. *
  1143. * @return int Maximum size in bytes
  1144. */
  1145. public static function max_upload_size()
  1146. {
  1147. // find max filesize value
  1148. $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
  1149. $max_postsize = parse_bytes(ini_get('post_max_size'));
  1150. if ($max_postsize && $max_postsize < $max_filesize) {
  1151. $max_filesize = $max_postsize;
  1152. }
  1153. return $max_filesize;
  1154. }
  1155. }