PageRenderTime 48ms CodeModel.GetById 27ms RepoModel.GetById 1ms app.codeStats 0ms

/includes/functions.php

https://code.google.com/p/phpbbex/
PHP | 2495 lines | 1684 code | 352 blank | 459 comment | 389 complexity | 0e9439b0338b208fcbc91ddb1a1ccff0 MD5 | raw file
Possible License(s): AGPL-1.0
  1. <?php
  2. /**
  3. *
  4. * @package phpBB3
  5. * @version $Id$
  6. * @copyright (c) 2005 phpBB Group
  7. * @license http://opensource.org/licenses/gpl-license.php GNU Public License
  8. *
  9. */
  10. /**
  11. * @ignore
  12. */
  13. if (!defined('IN_PHPBB'))
  14. {
  15. exit;
  16. }
  17. // Common global functions
  18. /**
  19. * set_var
  20. *
  21. * Set variable, used by {@link request_var the request_var function}
  22. *
  23. * @access private
  24. */
  25. function set_var(&$result, $var, $type, $multibyte = false)
  26. {
  27. settype($var, $type);
  28. $result = $var;
  29. if ($type == 'string')
  30. {
  31. $result = trim(htmlspecialchars(str_replace(array("\r\n", "\r", "\0"), array("\n", "\n", ''), $result), ENT_COMPAT, 'UTF-8'));
  32. if (!empty($result))
  33. {
  34. // Make sure multibyte characters are wellformed
  35. if ($multibyte)
  36. {
  37. if (!preg_match('/^./u', $result))
  38. {
  39. $result = '';
  40. }
  41. }
  42. else
  43. {
  44. // no multibyte, allow only ASCII (0-127)
  45. $result = preg_replace('/[\x80-\xFF]/', '?', $result);
  46. }
  47. }
  48. $result = (STRIP) ? stripslashes($result) : $result;
  49. }
  50. }
  51. /**
  52. * request_var
  53. *
  54. * Used to get passed variable
  55. */
  56. function request_var($var_name, $default, $multibyte = false, $cookie = false)
  57. {
  58. if (!$cookie && isset($_COOKIE[$var_name]))
  59. {
  60. if (!isset($_GET[$var_name]) && !isset($_POST[$var_name]))
  61. {
  62. return (is_array($default)) ? array() : $default;
  63. }
  64. $_REQUEST[$var_name] = isset($_POST[$var_name]) ? $_POST[$var_name] : $_GET[$var_name];
  65. }
  66. $super_global = ($cookie) ? '_COOKIE' : '_REQUEST';
  67. if (!isset($GLOBALS[$super_global][$var_name]) || is_array($GLOBALS[$super_global][$var_name]) != is_array($default))
  68. {
  69. return (is_array($default)) ? array() : $default;
  70. }
  71. $var = $GLOBALS[$super_global][$var_name];
  72. if (!is_array($default))
  73. {
  74. $type = gettype($default);
  75. }
  76. else
  77. {
  78. list($key_type, $type) = each($default);
  79. $type = gettype($type);
  80. $key_type = gettype($key_type);
  81. if ($type == 'array')
  82. {
  83. reset($default);
  84. $default = current($default);
  85. list($sub_key_type, $sub_type) = each($default);
  86. $sub_type = gettype($sub_type);
  87. $sub_type = ($sub_type == 'array') ? 'NULL' : $sub_type;
  88. $sub_key_type = gettype($sub_key_type);
  89. }
  90. }
  91. if (is_array($var))
  92. {
  93. $_var = $var;
  94. $var = array();
  95. foreach ($_var as $k => $v)
  96. {
  97. set_var($k, $k, $key_type);
  98. if ($type == 'array' && is_array($v))
  99. {
  100. foreach ($v as $_k => $_v)
  101. {
  102. if (is_array($_v))
  103. {
  104. $_v = null;
  105. }
  106. set_var($_k, $_k, $sub_key_type, $multibyte);
  107. set_var($var[$k][$_k], $_v, $sub_type, $multibyte);
  108. }
  109. }
  110. else
  111. {
  112. if ($type == 'array' || is_array($v))
  113. {
  114. $v = null;
  115. }
  116. set_var($var[$k], $v, $type, $multibyte);
  117. }
  118. }
  119. }
  120. else
  121. {
  122. set_var($var, $var, $type, $multibyte);
  123. }
  124. return $var;
  125. }
  126. /**
  127. * Set config value. Creates missing config entry.
  128. */
  129. function set_config($config_name, $config_value, $is_dynamic = false)
  130. {
  131. global $db, $cache, $config;
  132. $sql = 'UPDATE ' . CONFIG_TABLE . "
  133. SET config_value = '" . $db->sql_escape($config_value) . "'
  134. WHERE config_name = '" . $db->sql_escape($config_name) . "'";
  135. $db->sql_query($sql);
  136. if (!$db->sql_affectedrows() && !isset($config[$config_name]))
  137. {
  138. $sql = 'INSERT INTO ' . CONFIG_TABLE . ' ' . $db->sql_build_array('INSERT', array(
  139. 'config_name' => $config_name,
  140. 'config_value' => $config_value,
  141. 'is_dynamic' => ($is_dynamic) ? 1 : 0));
  142. $db->sql_query($sql);
  143. }
  144. $config[$config_name] = $config_value;
  145. if (!$is_dynamic)
  146. {
  147. $cache->destroy('config');
  148. }
  149. }
  150. /**
  151. * Set dynamic config value with arithmetic operation.
  152. */
  153. function set_config_count($config_name, $increment, $is_dynamic = false)
  154. {
  155. global $db, $cache;
  156. switch ($db->sql_layer)
  157. {
  158. case 'firebird':
  159. // Precision must be from 1 to 18
  160. $sql_update = 'CAST(CAST(config_value as DECIMAL(18, 0)) + ' . (int) $increment . ' as VARCHAR(255))';
  161. break;
  162. case 'postgres':
  163. // Need to cast to text first for PostgreSQL 7.x
  164. $sql_update = 'CAST(CAST(config_value::text as DECIMAL(255, 0)) + ' . (int) $increment . ' as VARCHAR(255))';
  165. break;
  166. // MySQL, SQlite, mssql, mssql_odbc, oracle
  167. default:
  168. $sql_update = 'config_value + ' . (int) $increment;
  169. break;
  170. }
  171. $db->sql_query('UPDATE ' . CONFIG_TABLE . ' SET config_value = ' . $sql_update . " WHERE config_name = '" . $db->sql_escape($config_name) . "'");
  172. if (!$is_dynamic)
  173. {
  174. $cache->destroy('config');
  175. }
  176. }
  177. /**
  178. * Generates an alphanumeric random string of given length
  179. *
  180. * @return string
  181. */
  182. function gen_rand_string($num_chars = 8)
  183. {
  184. // [a, z] + [0, 9] = 36
  185. return substr(strtoupper(base_convert(unique_id(), 16, 36)), 0, $num_chars);
  186. }
  187. /**
  188. * Generates a user-friendly alphanumeric random string of given length
  189. * We remove 0 and O so users cannot confuse those in passwords etc.
  190. *
  191. * @return string
  192. */
  193. function gen_rand_string_friendly($num_chars = 8)
  194. {
  195. $rand_str = unique_id();
  196. // Remove Z and Y from the base_convert(), replace 0 with Z and O with Y
  197. // [a, z] + [0, 9] - {z, y} = [a, z] + [0, 9] - {0, o} = 34
  198. $rand_str = str_replace(array('0', 'O'), array('Z', 'Y'), strtoupper(base_convert($rand_str, 16, 34)));
  199. return substr($rand_str, 0, $num_chars);
  200. }
  201. /**
  202. * Return unique id
  203. * @param string $extra additional entropy
  204. */
  205. function unique_id($extra = 'c')
  206. {
  207. static $dss_seeded = false;
  208. global $config;
  209. $val = $config['rand_seed'] . microtime();
  210. $val = md5($val);
  211. $config['rand_seed'] = md5($config['rand_seed'] . $val . $extra);
  212. if ($dss_seeded !== true && ($config['rand_seed_last_update'] < time() - rand(1,10)))
  213. {
  214. set_config('rand_seed_last_update', time(), true);
  215. set_config('rand_seed', $config['rand_seed'], true);
  216. $dss_seeded = true;
  217. }
  218. return substr($val, 4, 16);
  219. }
  220. /**
  221. * Wrapper for mt_rand() which allows swapping $min and $max parameters.
  222. *
  223. * PHP does not allow us to swap the order of the arguments for mt_rand() anymore.
  224. * (since PHP 5.3.4, see http://bugs.php.net/46587)
  225. *
  226. * @param int $min Lowest value to be returned
  227. * @param int $max Highest value to be returned
  228. *
  229. * @return int Random integer between $min and $max (or $max and $min)
  230. */
  231. function phpbb_mt_rand($min, $max)
  232. {
  233. return ($min > $max) ? mt_rand($max, $min) : mt_rand($min, $max);
  234. }
  235. /**
  236. * Wrapper for getdate() which returns the equivalent array for UTC timestamps.
  237. *
  238. * @param int $time Unix timestamp (optional)
  239. *
  240. * @return array Returns an associative array of information related to the timestamp.
  241. * See http://www.php.net/manual/en/function.getdate.php
  242. */
  243. function phpbb_gmgetdate($time = false)
  244. {
  245. if ($time === false)
  246. {
  247. $time = time();
  248. }
  249. // getdate() interprets timestamps in local time.
  250. // What follows uses the fact that getdate() and
  251. // date('Z') balance each other out.
  252. return getdate($time - date('Z'));
  253. }
  254. /**
  255. * Return formatted string for filesizes
  256. *
  257. * @param int $value filesize in bytes
  258. * @param bool $string_only true if language string should be returned
  259. * @param array $allowed_units only allow these units (data array indexes)
  260. *
  261. * @return mixed data array if $string_only is false
  262. * @author bantu
  263. */
  264. function get_formatted_filesize($value, $string_only = true, $allowed_units = false)
  265. {
  266. global $user;
  267. $available_units = array(
  268. 'gb' => array(
  269. 'min' => 1073741824, // pow(2, 30)
  270. 'index' => 3,
  271. 'si_unit' => 'GB',
  272. 'iec_unit' => 'GIB',
  273. ),
  274. 'mb' => array(
  275. 'min' => 1048576, // pow(2, 20)
  276. 'index' => 2,
  277. 'si_unit' => 'MB',
  278. 'iec_unit' => 'MIB',
  279. ),
  280. 'kb' => array(
  281. 'min' => 1024, // pow(2, 10)
  282. 'index' => 1,
  283. 'si_unit' => 'KB',
  284. 'iec_unit' => 'KIB',
  285. ),
  286. 'b' => array(
  287. 'min' => 0,
  288. 'index' => 0,
  289. 'si_unit' => 'BYTES', // Language index
  290. 'iec_unit' => 'BYTES', // Language index
  291. ),
  292. );
  293. foreach ($available_units as $si_identifier => $unit_info)
  294. {
  295. if (!empty($allowed_units) && $si_identifier != 'b' && !in_array($si_identifier, $allowed_units))
  296. {
  297. continue;
  298. }
  299. if ($value >= $unit_info['min'])
  300. {
  301. $unit_info['si_identifier'] = $si_identifier;
  302. break;
  303. }
  304. }
  305. unset($available_units);
  306. for ($i = 0; $i < $unit_info['index']; $i++)
  307. {
  308. $value /= 1024;
  309. }
  310. $value = round($value, 2);
  311. // Lookup units in language dictionary
  312. $unit_info['si_unit'] = (isset($user->lang[$unit_info['si_unit']])) ? $user->lang[$unit_info['si_unit']] : $unit_info['si_unit'];
  313. $unit_info['iec_unit'] = (isset($user->lang[$unit_info['iec_unit']])) ? $user->lang[$unit_info['iec_unit']] : $unit_info['iec_unit'];
  314. // Default to IEC
  315. $unit_info['unit'] = $unit_info['iec_unit'];
  316. if (!$string_only)
  317. {
  318. $unit_info['value'] = $value;
  319. return $unit_info;
  320. }
  321. return $value . ' ' . $unit_info['unit'];
  322. }
  323. /**
  324. * Determine whether we are approaching the maximum execution time. Should be called once
  325. * at the beginning of the script in which it's used.
  326. * @return bool Either true if the maximum execution time is nearly reached, or false
  327. * if some time is still left.
  328. */
  329. function still_on_time($extra_time = 15)
  330. {
  331. static $max_execution_time, $start_time;
  332. $time = explode(' ', microtime());
  333. $current_time = $time[0] + $time[1];
  334. if (empty($max_execution_time))
  335. {
  336. $max_execution_time = (function_exists('ini_get')) ? (int) @ini_get('max_execution_time') : (int) @get_cfg_var('max_execution_time');
  337. // If zero, then set to something higher to not let the user catch the ten seconds barrier.
  338. if ($max_execution_time === 0)
  339. {
  340. $max_execution_time = 50 + $extra_time;
  341. }
  342. $max_execution_time = min(max(10, ($max_execution_time - $extra_time)), 50);
  343. // For debugging purposes
  344. // $max_execution_time = 10;
  345. global $starttime;
  346. $start_time = (empty($starttime)) ? $current_time : $starttime;
  347. }
  348. return (ceil($current_time - $start_time) < $max_execution_time) ? true : false;
  349. }
  350. /**
  351. *
  352. * @version Version 0.1 / slightly modified for phpBB 3.0.x (using $H$ as hash type identifier)
  353. *
  354. * Portable PHP password hashing framework.
  355. *
  356. * Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in
  357. * the public domain.
  358. *
  359. * There's absolutely no warranty.
  360. *
  361. * The homepage URL for this framework is:
  362. *
  363. * http://www.openwall.com/phpass/
  364. *
  365. * Please be sure to update the Version line if you edit this file in any way.
  366. * It is suggested that you leave the main version number intact, but indicate
  367. * your project name (after the slash) and add your own revision information.
  368. *
  369. * Please do not change the "private" password hashing method implemented in
  370. * here, thereby making your hashes incompatible. However, if you must, please
  371. * change the hash type identifier (the "$P$") to something different.
  372. *
  373. * Obviously, since this code is in the public domain, the above are not
  374. * requirements (there can be none), but merely suggestions.
  375. *
  376. *
  377. * Hash the password
  378. */
  379. function phpbb_hash($password)
  380. {
  381. $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
  382. $random_state = unique_id();
  383. $random = '';
  384. $count = 6;
  385. if (($fh = @fopen('/dev/urandom', 'rb')))
  386. {
  387. $random = fread($fh, $count);
  388. fclose($fh);
  389. }
  390. if (strlen($random) < $count)
  391. {
  392. $random = '';
  393. for ($i = 0; $i < $count; $i += 16)
  394. {
  395. $random_state = md5(unique_id() . $random_state);
  396. $random .= pack('H*', md5($random_state));
  397. }
  398. $random = substr($random, 0, $count);
  399. }
  400. $hash = _hash_crypt_private($password, _hash_gensalt_private($random, $itoa64), $itoa64);
  401. if (strlen($hash) == 34)
  402. {
  403. return $hash;
  404. }
  405. return md5($password);
  406. }
  407. /**
  408. * Check for correct password
  409. *
  410. * @param string $password The password in plain text
  411. * @param string $hash The stored password hash
  412. *
  413. * @return bool Returns true if the password is correct, false if not.
  414. */
  415. function phpbb_check_hash($password, $hash)
  416. {
  417. $itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
  418. if (strlen($hash) == 34)
  419. {
  420. return (_hash_crypt_private($password, $hash, $itoa64) === $hash) ? true : false;
  421. }
  422. return (md5($password) === $hash) ? true : false;
  423. }
  424. /**
  425. * Generate salt for hash generation
  426. */
  427. function _hash_gensalt_private($input, &$itoa64, $iteration_count_log2 = 6)
  428. {
  429. if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31)
  430. {
  431. $iteration_count_log2 = 8;
  432. }
  433. $output = '$H$';
  434. $output .= $itoa64[min($iteration_count_log2 + ((PHP_VERSION >= 5) ? 5 : 3), 30)];
  435. $output .= _hash_encode64($input, 6, $itoa64);
  436. return $output;
  437. }
  438. /**
  439. * Encode hash
  440. */
  441. function _hash_encode64($input, $count, &$itoa64)
  442. {
  443. $output = '';
  444. $i = 0;
  445. do
  446. {
  447. $value = ord($input[$i++]);
  448. $output .= $itoa64[$value & 0x3f];
  449. if ($i < $count)
  450. {
  451. $value |= ord($input[$i]) << 8;
  452. }
  453. $output .= $itoa64[($value >> 6) & 0x3f];
  454. if ($i++ >= $count)
  455. {
  456. break;
  457. }
  458. if ($i < $count)
  459. {
  460. $value |= ord($input[$i]) << 16;
  461. }
  462. $output .= $itoa64[($value >> 12) & 0x3f];
  463. if ($i++ >= $count)
  464. {
  465. break;
  466. }
  467. $output .= $itoa64[($value >> 18) & 0x3f];
  468. }
  469. while ($i < $count);
  470. return $output;
  471. }
  472. /**
  473. * The crypt function/replacement
  474. */
  475. function _hash_crypt_private($password, $setting, &$itoa64)
  476. {
  477. $output = '*';
  478. // Check for correct hash
  479. if (substr($setting, 0, 3) != '$H$' && substr($setting, 0, 3) != '$P$')
  480. {
  481. return $output;
  482. }
  483. $count_log2 = strpos($itoa64, $setting[3]);
  484. if ($count_log2 < 7 || $count_log2 > 30)
  485. {
  486. return $output;
  487. }
  488. $count = 1 << $count_log2;
  489. $salt = substr($setting, 4, 8);
  490. if (strlen($salt) != 8)
  491. {
  492. return $output;
  493. }
  494. /**
  495. * We're kind of forced to use MD5 here since it's the only
  496. * cryptographic primitive available in all versions of PHP
  497. * currently in use. To implement our own low-level crypto
  498. * in PHP would result in much worse performance and
  499. * consequently in lower iteration counts and hashes that are
  500. * quicker to crack (by non-PHP code).
  501. */
  502. if (PHP_VERSION >= 5)
  503. {
  504. $hash = md5($salt . $password, true);
  505. do
  506. {
  507. $hash = md5($hash . $password, true);
  508. }
  509. while (--$count);
  510. }
  511. else
  512. {
  513. $hash = pack('H*', md5($salt . $password));
  514. do
  515. {
  516. $hash = pack('H*', md5($hash . $password));
  517. }
  518. while (--$count);
  519. }
  520. $output = substr($setting, 0, 12);
  521. $output .= _hash_encode64($hash, 16, $itoa64);
  522. return $output;
  523. }
  524. /**
  525. * Hashes an email address to a big integer
  526. *
  527. * @param string $email Email address
  528. *
  529. * @return string Unsigned Big Integer
  530. */
  531. function phpbb_email_hash($email)
  532. {
  533. return sprintf('%u', crc32(strtolower($email))) . strlen($email);
  534. }
  535. /**
  536. * Wrapper for version_compare() that allows using uppercase A and B
  537. * for alpha and beta releases.
  538. *
  539. * See http://www.php.net/manual/en/function.version-compare.php
  540. *
  541. * @param string $version1 First version number
  542. * @param string $version2 Second version number
  543. * @param string $operator Comparison operator (optional)
  544. *
  545. * @return mixed Boolean (true, false) if comparison operator is specified.
  546. * Integer (-1, 0, 1) otherwise.
  547. */
  548. function phpbb_version_compare($version1, $version2, $operator = null)
  549. {
  550. $version1 = strtolower($version1);
  551. $version2 = strtolower($version2);
  552. if (is_null($operator))
  553. {
  554. return version_compare($version1, $version2);
  555. }
  556. else
  557. {
  558. return version_compare($version1, $version2, $operator);
  559. }
  560. }
  561. /**
  562. * Global function for chmodding directories and files for internal use
  563. *
  564. * This function determines owner and group whom the file belongs to and user and group of PHP and then set safest possible file permissions.
  565. * The function determines owner and group from common.php file and sets the same to the provided file.
  566. * The function uses bit fields to build the permissions.
  567. * The function sets the appropiate execute bit on directories.
  568. *
  569. * Supported constants representing bit fields are:
  570. *
  571. * CHMOD_ALL - all permissions (7)
  572. * CHMOD_READ - read permission (4)
  573. * CHMOD_WRITE - write permission (2)
  574. * CHMOD_EXECUTE - execute permission (1)
  575. *
  576. * NOTE: The function uses POSIX extension and fileowner()/filegroup() functions. If any of them is disabled, this function tries to build proper permissions, by calling is_readable() and is_writable() functions.
  577. *
  578. * @param string $filename The file/directory to be chmodded
  579. * @param int $perms Permissions to set
  580. *
  581. * @return bool true on success, otherwise false
  582. * @author faw, phpBB Group
  583. */
  584. function phpbb_chmod($filename, $perms = CHMOD_READ)
  585. {
  586. static $_chmod_info;
  587. // Return if the file no longer exists.
  588. if (!file_exists($filename))
  589. {
  590. return false;
  591. }
  592. // Determine some common vars
  593. if (empty($_chmod_info))
  594. {
  595. if (!function_exists('fileowner') || !function_exists('filegroup'))
  596. {
  597. // No need to further determine owner/group - it is unknown
  598. $_chmod_info['process'] = false;
  599. }
  600. else
  601. {
  602. global $phpbb_root_path, $phpEx;
  603. // Determine owner/group of common.php file and the filename we want to change here
  604. $common_php_owner = @fileowner($phpbb_root_path . 'common.' . $phpEx);
  605. $common_php_group = @filegroup($phpbb_root_path . 'common.' . $phpEx);
  606. // And the owner and the groups PHP is running under.
  607. $php_uid = (function_exists('posix_getuid')) ? @posix_getuid() : false;
  608. $php_gids = (function_exists('posix_getgroups')) ? @posix_getgroups() : false;
  609. // If we are unable to get owner/group, then do not try to set them by guessing
  610. if (!$php_uid || empty($php_gids) || !$common_php_owner || !$common_php_group)
  611. {
  612. $_chmod_info['process'] = false;
  613. }
  614. else
  615. {
  616. $_chmod_info = array(
  617. 'process' => true,
  618. 'common_owner' => $common_php_owner,
  619. 'common_group' => $common_php_group,
  620. 'php_uid' => $php_uid,
  621. 'php_gids' => $php_gids,
  622. );
  623. }
  624. }
  625. }
  626. if ($_chmod_info['process'])
  627. {
  628. $file_uid = @fileowner($filename);
  629. $file_gid = @filegroup($filename);
  630. // Change owner
  631. if (@chown($filename, $_chmod_info['common_owner']))
  632. {
  633. clearstatcache();
  634. $file_uid = @fileowner($filename);
  635. }
  636. // Change group
  637. if (@chgrp($filename, $_chmod_info['common_group']))
  638. {
  639. clearstatcache();
  640. $file_gid = @filegroup($filename);
  641. }
  642. // If the file_uid/gid now match the one from common.php we can process further, else we are not able to change something
  643. if ($file_uid != $_chmod_info['common_owner'] || $file_gid != $_chmod_info['common_group'])
  644. {
  645. $_chmod_info['process'] = false;
  646. }
  647. }
  648. // Still able to process?
  649. if ($_chmod_info['process'])
  650. {
  651. if ($file_uid == $_chmod_info['php_uid'])
  652. {
  653. $php = 'owner';
  654. }
  655. else if (in_array($file_gid, $_chmod_info['php_gids']))
  656. {
  657. $php = 'group';
  658. }
  659. else
  660. {
  661. // Since we are setting the everyone bit anyway, no need to do expensive operations
  662. $_chmod_info['process'] = false;
  663. }
  664. }
  665. // We are not able to determine or change something
  666. if (!$_chmod_info['process'])
  667. {
  668. $php = 'other';
  669. }
  670. // Owner always has read/write permission
  671. $owner = CHMOD_READ | CHMOD_WRITE;
  672. if (is_dir($filename))
  673. {
  674. $owner |= CHMOD_EXECUTE;
  675. // Only add execute bit to the permission if the dir needs to be readable
  676. if ($perms & CHMOD_READ)
  677. {
  678. $perms |= CHMOD_EXECUTE;
  679. }
  680. }
  681. switch ($php)
  682. {
  683. case 'owner':
  684. $result = @chmod($filename, ($owner << 6) + (0 << 3) + (0 << 0));
  685. clearstatcache();
  686. if (is_readable($filename) && phpbb_is_writable($filename))
  687. {
  688. break;
  689. }
  690. case 'group':
  691. $result = @chmod($filename, ($owner << 6) + ($perms << 3) + (0 << 0));
  692. clearstatcache();
  693. if ((!($perms & CHMOD_READ) || is_readable($filename)) && (!($perms & CHMOD_WRITE) || phpbb_is_writable($filename)))
  694. {
  695. break;
  696. }
  697. case 'other':
  698. $result = @chmod($filename, ($owner << 6) + ($perms << 3) + ($perms << 0));
  699. clearstatcache();
  700. if ((!($perms & CHMOD_READ) || is_readable($filename)) && (!($perms & CHMOD_WRITE) || phpbb_is_writable($filename)))
  701. {
  702. break;
  703. }
  704. default:
  705. return false;
  706. break;
  707. }
  708. return $result;
  709. }
  710. /**
  711. * Test if a file/directory is writable
  712. *
  713. * This function calls the native is_writable() when not running under
  714. * Windows and it is not disabled.
  715. *
  716. * @param string $file Path to perform write test on
  717. * @return bool True when the path is writable, otherwise false.
  718. */
  719. function phpbb_is_writable($file)
  720. {
  721. if (strtolower(substr(PHP_OS, 0, 3)) === 'win' || !function_exists('is_writable'))
  722. {
  723. if (file_exists($file))
  724. {
  725. // Canonicalise path to absolute path
  726. $file = phpbb_realpath($file);
  727. if (is_dir($file))
  728. {
  729. // Test directory by creating a file inside the directory
  730. $result = @tempnam($file, 'i_w');
  731. if (is_string($result) && file_exists($result))
  732. {
  733. unlink($result);
  734. // Ensure the file is actually in the directory (returned realpathed)
  735. return (strpos($result, $file) === 0) ? true : false;
  736. }
  737. }
  738. else
  739. {
  740. $handle = @fopen($file, 'r+');
  741. if (is_resource($handle))
  742. {
  743. fclose($handle);
  744. return true;
  745. }
  746. }
  747. }
  748. else
  749. {
  750. // file does not exist test if we can write to the directory
  751. $dir = dirname($file);
  752. if (file_exists($dir) && is_dir($dir) && phpbb_is_writable($dir))
  753. {
  754. return true;
  755. }
  756. }
  757. return false;
  758. }
  759. else
  760. {
  761. return is_writable($file);
  762. }
  763. }
  764. // Compatibility functions
  765. if (!function_exists('array_combine'))
  766. {
  767. /**
  768. * A wrapper for the PHP5 function array_combine()
  769. * @param array $keys contains keys for the resulting array
  770. * @param array $values contains values for the resulting array
  771. *
  772. * @return Returns an array by using the values from the keys array as keys and the
  773. * values from the values array as the corresponding values. Returns false if the
  774. * number of elements for each array isn't equal or if the arrays are empty.
  775. */
  776. function array_combine($keys, $values)
  777. {
  778. $keys = array_values($keys);
  779. $values = array_values($values);
  780. $n = sizeof($keys);
  781. $m = sizeof($values);
  782. if (!$n || !$m || ($n != $m))
  783. {
  784. return false;
  785. }
  786. $combined = array();
  787. for ($i = 0; $i < $n; $i++)
  788. {
  789. $combined[$keys[$i]] = $values[$i];
  790. }
  791. return $combined;
  792. }
  793. }
  794. if (!function_exists('str_split'))
  795. {
  796. /**
  797. * A wrapper for the PHP5 function str_split()
  798. * @param array $string contains the string to be converted
  799. * @param array $split_length contains the length of each chunk
  800. *
  801. * @return Converts a string to an array. If the optional split_length parameter is specified,
  802. * the returned array will be broken down into chunks with each being split_length in length,
  803. * otherwise each chunk will be one character in length. FALSE is returned if split_length is
  804. * less than 1. If the split_length length exceeds the length of string, the entire string is
  805. * returned as the first (and only) array element.
  806. */
  807. function str_split($string, $split_length = 1)
  808. {
  809. if ($split_length < 1)
  810. {
  811. return false;
  812. }
  813. else if ($split_length >= strlen($string))
  814. {
  815. return array($string);
  816. }
  817. else
  818. {
  819. preg_match_all('#.{1,' . $split_length . '}#s', $string, $matches);
  820. return $matches[0];
  821. }
  822. }
  823. }
  824. if (!function_exists('stripos'))
  825. {
  826. /**
  827. * A wrapper for the PHP5 function stripos
  828. * Find position of first occurrence of a case-insensitive string
  829. *
  830. * @param string $haystack is the string to search in
  831. * @param string $needle is the string to search for
  832. *
  833. * @return mixed Returns the numeric position of the first occurrence of needle in the haystack string. Unlike strpos(), stripos() is case-insensitive.
  834. * Note that the needle may be a string of one or more characters.
  835. * If needle is not found, stripos() will return boolean FALSE.
  836. */
  837. function stripos($haystack, $needle)
  838. {
  839. if (preg_match('#' . preg_quote($needle, '#') . '#i', $haystack, $m))
  840. {
  841. return strpos($haystack, $m[0]);
  842. }
  843. return false;
  844. }
  845. }
  846. /**
  847. * Checks if a path ($path) is absolute or relative
  848. *
  849. * @param string $path Path to check absoluteness of
  850. * @return boolean
  851. */
  852. function is_absolute($path)
  853. {
  854. return ($path[0] == '/' || (DIRECTORY_SEPARATOR == '\\' && preg_match('#^[a-z]:[/\\\]#i', $path))) ? true : false;
  855. }
  856. /**
  857. * @author Chris Smith <chris@project-minerva.org>
  858. * @copyright 2006 Project Minerva Team
  859. * @param string $path The path which we should attempt to resolve.
  860. * @return mixed
  861. */
  862. function phpbb_own_realpath($path)
  863. {
  864. // Now to perform funky shizzle
  865. // Switch to use UNIX slashes
  866. $path = str_replace(DIRECTORY_SEPARATOR, '/', $path);
  867. $path_prefix = '';
  868. // Determine what sort of path we have
  869. if (is_absolute($path))
  870. {
  871. $absolute = true;
  872. if ($path[0] == '/')
  873. {
  874. // Absolute path, *NIX style
  875. $path_prefix = '';
  876. }
  877. else
  878. {
  879. // Absolute path, Windows style
  880. // Remove the drive letter and colon
  881. $path_prefix = $path[0] . ':';
  882. $path = substr($path, 2);
  883. }
  884. }
  885. else
  886. {
  887. // Relative Path
  888. // Prepend the current working directory
  889. if (function_exists('getcwd'))
  890. {
  891. // This is the best method, hopefully it is enabled!
  892. $path = str_replace(DIRECTORY_SEPARATOR, '/', getcwd()) . '/' . $path;
  893. $absolute = true;
  894. if (preg_match('#^[a-z]:#i', $path))
  895. {
  896. $path_prefix = $path[0] . ':';
  897. $path = substr($path, 2);
  898. }
  899. else
  900. {
  901. $path_prefix = '';
  902. }
  903. }
  904. else if (isset($_SERVER['SCRIPT_FILENAME']) && !empty($_SERVER['SCRIPT_FILENAME']))
  905. {
  906. // Warning: If chdir() has been used this will lie!
  907. // Warning: This has some problems sometime (CLI can create them easily)
  908. $path = str_replace(DIRECTORY_SEPARATOR, '/', dirname($_SERVER['SCRIPT_FILENAME'])) . '/' . $path;
  909. $absolute = true;
  910. $path_prefix = '';
  911. }
  912. else
  913. {
  914. // We have no way of getting the absolute path, just run on using relative ones.
  915. $absolute = false;
  916. $path_prefix = '.';
  917. }
  918. }
  919. // Remove any repeated slashes
  920. $path = preg_replace('#/{2,}#', '/', $path);
  921. // Remove the slashes from the start and end of the path
  922. $path = trim($path, '/');
  923. // Break the string into little bits for us to nibble on
  924. $bits = explode('/', $path);
  925. // Remove any . in the path, renumber array for the loop below
  926. $bits = array_values(array_diff($bits, array('.')));
  927. // Lets get looping, run over and resolve any .. (up directory)
  928. for ($i = 0, $max = sizeof($bits); $i < $max; $i++)
  929. {
  930. // @todo Optimise
  931. if ($bits[$i] == '..' )
  932. {
  933. if (isset($bits[$i - 1]))
  934. {
  935. if ($bits[$i - 1] != '..')
  936. {
  937. // We found a .. and we are able to traverse upwards, lets do it!
  938. unset($bits[$i]);
  939. unset($bits[$i - 1]);
  940. $i -= 2;
  941. $max -= 2;
  942. $bits = array_values($bits);
  943. }
  944. }
  945. else if ($absolute) // ie. !isset($bits[$i - 1]) && $absolute
  946. {
  947. // We have an absolute path trying to descend above the root of the filesystem
  948. // ... Error!
  949. return false;
  950. }
  951. }
  952. }
  953. // Prepend the path prefix
  954. array_unshift($bits, $path_prefix);
  955. $resolved = '';
  956. $max = sizeof($bits) - 1;
  957. // Check if we are able to resolve symlinks, Windows cannot.
  958. $symlink_resolve = (function_exists('readlink')) ? true : false;
  959. foreach ($bits as $i => $bit)
  960. {
  961. if (@is_dir("$resolved/$bit") || ($i == $max && @is_file("$resolved/$bit")))
  962. {
  963. // Path Exists
  964. if ($symlink_resolve && is_link("$resolved/$bit") && ($link = readlink("$resolved/$bit")))
  965. {
  966. // Resolved a symlink.
  967. $resolved = $link . (($i == $max) ? '' : '/');
  968. continue;
  969. }
  970. }
  971. else
  972. {
  973. // Something doesn't exist here!
  974. // This is correct realpath() behaviour but sadly open_basedir and safe_mode make this problematic
  975. // return false;
  976. }
  977. $resolved .= $bit . (($i == $max) ? '' : '/');
  978. }
  979. // @todo If the file exists fine and open_basedir only has one path we should be able to prepend it
  980. // because we must be inside that basedir, the question is where...
  981. // @internal The slash in is_dir() gets around an open_basedir restriction
  982. if (!@file_exists($resolved) || (!@is_dir($resolved . '/') && !is_file($resolved)))
  983. {
  984. return false;
  985. }
  986. // Put the slashes back to the native operating systems slashes
  987. $resolved = str_replace('/', DIRECTORY_SEPARATOR, $resolved);
  988. // Check for DIRECTORY_SEPARATOR at the end (and remove it!)
  989. if (substr($resolved, -1) == DIRECTORY_SEPARATOR)
  990. {
  991. return substr($resolved, 0, -1);
  992. }
  993. return $resolved; // We got here, in the end!
  994. }
  995. if (!function_exists('realpath'))
  996. {
  997. /**
  998. * A wrapper for realpath
  999. * @ignore
  1000. */
  1001. function phpbb_realpath($path)
  1002. {
  1003. return phpbb_own_realpath($path);
  1004. }
  1005. }
  1006. else
  1007. {
  1008. /**
  1009. * A wrapper for realpath
  1010. */
  1011. function phpbb_realpath($path)
  1012. {
  1013. $realpath = realpath($path);
  1014. // Strangely there are provider not disabling realpath but returning strange values. :o
  1015. // We at least try to cope with them.
  1016. if ($realpath === $path || $realpath === false)
  1017. {
  1018. return phpbb_own_realpath($path);
  1019. }
  1020. // Check for DIRECTORY_SEPARATOR at the end (and remove it!)
  1021. if (substr($realpath, -1) == DIRECTORY_SEPARATOR)
  1022. {
  1023. $realpath = substr($realpath, 0, -1);
  1024. }
  1025. return $realpath;
  1026. }
  1027. }
  1028. if (!function_exists('htmlspecialchars_decode'))
  1029. {
  1030. /**
  1031. * A wrapper for htmlspecialchars_decode
  1032. * @ignore
  1033. */
  1034. function htmlspecialchars_decode($string, $quote_style = ENT_COMPAT)
  1035. {
  1036. return strtr($string, array_flip(get_html_translation_table(HTML_SPECIALCHARS, $quote_style)));
  1037. }
  1038. }
  1039. // functions used for building option fields
  1040. /**
  1041. * Pick a language, any language ...
  1042. */
  1043. function language_select($default = '')
  1044. {
  1045. global $db;
  1046. $sql = 'SELECT lang_iso, lang_local_name
  1047. FROM ' . LANG_TABLE . '
  1048. ORDER BY lang_english_name';
  1049. $result = $db->sql_query($sql);
  1050. $lang_options = '';
  1051. while ($row = $db->sql_fetchrow($result))
  1052. {
  1053. $selected = ($row['lang_iso'] == $default) ? ' selected="selected"' : '';
  1054. $lang_options .= '<option value="' . $row['lang_iso'] . '"' . $selected . '>' . $row['lang_local_name'] . '</option>';
  1055. }
  1056. $db->sql_freeresult($result);
  1057. return $lang_options;
  1058. }
  1059. /**
  1060. * Pick a template/theme combo,
  1061. */
  1062. function style_select($default = '', $all = false)
  1063. {
  1064. global $db;
  1065. $sql_where = (!$all) ? 'WHERE style_active = 1 ' : '';
  1066. $sql = 'SELECT style_id, style_name
  1067. FROM ' . STYLES_TABLE . "
  1068. $sql_where
  1069. ORDER BY style_name";
  1070. $result = $db->sql_query($sql);
  1071. $style_options = '';
  1072. while ($row = $db->sql_fetchrow($result))
  1073. {
  1074. $selected = ($row['style_id'] == $default) ? ' selected="selected"' : '';
  1075. $style_options .= '<option value="' . $row['style_id'] . '"' . $selected . '>' . $row['style_name'] . '</option>';
  1076. }
  1077. $db->sql_freeresult($result);
  1078. return $style_options;
  1079. }
  1080. /**
  1081. * Pick a timezone
  1082. */
  1083. function tz_select($default = '', $truncate = false)
  1084. {
  1085. global $user;
  1086. $tz_select = '';
  1087. foreach ($user->lang['tz_zones'] as $offset => $zone)
  1088. {
  1089. if ($truncate)
  1090. {
  1091. $zone_trunc = truncate_string($zone, 50, 255, false, '...');
  1092. }
  1093. else
  1094. {
  1095. $zone_trunc = $zone;
  1096. }
  1097. if (is_numeric($offset))
  1098. {
  1099. $selected = ($offset == $default) ? ' selected="selected"' : '';
  1100. $tz_select .= '<option title="' . $zone . '" value="' . $offset . '"' . $selected . '>' . $zone_trunc . '</option>';
  1101. }
  1102. }
  1103. return $tz_select;
  1104. }
  1105. // Functions handling topic/post tracking/marking
  1106. /**
  1107. * Marks a topic/forum as read
  1108. * Marks a topic as posted to
  1109. *
  1110. * @param int $user_id can only be used with $mode == 'post'
  1111. */
  1112. function markread($mode, $forum_id = false, $topic_id = false, $post_time = 0, $user_id = 0)
  1113. {
  1114. global $db, $user, $config;
  1115. if ($mode == 'all')
  1116. {
  1117. if ($forum_id === false || !sizeof($forum_id))
  1118. {
  1119. if ($config['load_db_lastread'] && $user->data['is_registered'])
  1120. {
  1121. // Mark all forums read (index page)
  1122. $db->sql_query('DELETE FROM ' . TOPICS_TRACK_TABLE . " WHERE user_id = {$user->data['user_id']}");
  1123. $db->sql_query('DELETE FROM ' . FORUMS_TRACK_TABLE . " WHERE user_id = {$user->data['user_id']}");
  1124. $db->sql_query('UPDATE ' . USERS_TABLE . ' SET user_lastmark = ' . time() . " WHERE user_id = {$user->data['user_id']}");
  1125. }
  1126. else if ($config['load_anon_lastread'] || $user->data['is_registered'])
  1127. {
  1128. $tracking_topics = (isset($_COOKIE[$config['cookie_name'] . '_track'])) ? ((STRIP) ? stripslashes($_COOKIE[$config['cookie_name'] . '_track']) : $_COOKIE[$config['cookie_name'] . '_track']) : '';
  1129. $tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
  1130. unset($tracking_topics['tf']);
  1131. unset($tracking_topics['t']);
  1132. unset($tracking_topics['f']);
  1133. $tracking_topics['l'] = base_convert(time() - $config['board_startdate'], 10, 36);
  1134. $user->set_cookie('track', tracking_serialize($tracking_topics), time() + 31536000);
  1135. $_COOKIE[$config['cookie_name'] . '_track'] = (STRIP) ? addslashes(tracking_serialize($tracking_topics)) : tracking_serialize($tracking_topics);
  1136. unset($tracking_topics);
  1137. if ($user->data['is_registered'])
  1138. {
  1139. $db->sql_query('UPDATE ' . USERS_TABLE . ' SET user_lastmark = ' . time() . " WHERE user_id = {$user->data['user_id']}");
  1140. }
  1141. }
  1142. }
  1143. return;
  1144. }
  1145. else if ($mode == 'topics')
  1146. {
  1147. // Mark all topics in forums read
  1148. if (!is_array($forum_id))
  1149. {
  1150. $forum_id = array($forum_id);
  1151. }
  1152. // Add 0 to forums array to mark global announcements correctly
  1153. // $forum_id[] = 0;
  1154. if ($config['load_db_lastread'] && $user->data['is_registered'])
  1155. {
  1156. $sql = 'DELETE FROM ' . TOPICS_TRACK_TABLE . "
  1157. WHERE user_id = {$user->data['user_id']}
  1158. AND " . $db->sql_in_set('forum_id', $forum_id);
  1159. $db->sql_query($sql);
  1160. $sql = 'SELECT forum_id
  1161. FROM ' . FORUMS_TRACK_TABLE . "
  1162. WHERE user_id = {$user->data['user_id']}
  1163. AND " . $db->sql_in_set('forum_id', $forum_id);
  1164. $result = $db->sql_query($sql);
  1165. $sql_update = array();
  1166. while ($row = $db->sql_fetchrow($result))
  1167. {
  1168. $sql_update[] = (int) $row['forum_id'];
  1169. }
  1170. $db->sql_freeresult($result);
  1171. if (sizeof($sql_update))
  1172. {
  1173. $sql = 'UPDATE ' . FORUMS_TRACK_TABLE . '
  1174. SET mark_time = ' . time() . "
  1175. WHERE user_id = {$user->data['user_id']}
  1176. AND " . $db->sql_in_set('forum_id', $sql_update);
  1177. $db->sql_query($sql);
  1178. }
  1179. if ($sql_insert = array_diff($forum_id, $sql_update))
  1180. {
  1181. $sql_ary = array();
  1182. foreach ($sql_insert as $f_id)
  1183. {
  1184. $sql_ary[] = array(
  1185. 'user_id' => (int) $user->data['user_id'],
  1186. 'forum_id' => (int) $f_id,
  1187. 'mark_time' => time()
  1188. );
  1189. }
  1190. $db->sql_multi_insert(FORUMS_TRACK_TABLE, $sql_ary);
  1191. }
  1192. }
  1193. else if ($config['load_anon_lastread'] || $user->data['is_registered'])
  1194. {
  1195. $tracking = (isset($_COOKIE[$config['cookie_name'] . '_track'])) ? ((STRIP) ? stripslashes($_COOKIE[$config['cookie_name'] . '_track']) : $_COOKIE[$config['cookie_name'] . '_track']) : '';
  1196. $tracking = ($tracking) ? tracking_unserialize($tracking) : array();
  1197. foreach ($forum_id as $f_id)
  1198. {
  1199. $topic_ids36 = (isset($tracking['tf'][$f_id])) ? $tracking['tf'][$f_id] : array();
  1200. if (isset($tracking['tf'][$f_id]))
  1201. {
  1202. unset($tracking['tf'][$f_id]);
  1203. }
  1204. foreach ($topic_ids36 as $topic_id36)
  1205. {
  1206. unset($tracking['t'][$topic_id36]);
  1207. }
  1208. if (isset($tracking['f'][$f_id]))
  1209. {
  1210. unset($tracking['f'][$f_id]);
  1211. }
  1212. $tracking['f'][$f_id] = base_convert(time() - $config['board_startdate'], 10, 36);
  1213. }
  1214. if (isset($tracking['tf']) && empty($tracking['tf']))
  1215. {
  1216. unset($tracking['tf']);
  1217. }
  1218. $user->set_cookie('track', tracking_serialize($tracking), time() + 31536000);
  1219. $_COOKIE[$config['cookie_name'] . '_track'] = (STRIP) ? addslashes(tracking_serialize($tracking)) : tracking_serialize($tracking);
  1220. unset($tracking);
  1221. }
  1222. return;
  1223. }
  1224. else if ($mode == 'topic')
  1225. {
  1226. if ($topic_id === false || $forum_id === false)
  1227. {
  1228. return;
  1229. }
  1230. if ($config['load_db_lastread'] && $user->data['is_registered'])
  1231. {
  1232. $sql = 'UPDATE ' . TOPICS_TRACK_TABLE . '
  1233. SET mark_time = ' . (($post_time) ? $post_time : time()) . "
  1234. WHERE user_id = {$user->data['user_id']}
  1235. AND topic_id = $topic_id";
  1236. $db->sql_query($sql);
  1237. // insert row
  1238. if (!$db->sql_affectedrows())
  1239. {
  1240. $db->sql_return_on_error(true);
  1241. $sql_ary = array(
  1242. 'user_id' => (int) $user->data['user_id'],
  1243. 'topic_id' => (int) $topic_id,
  1244. 'forum_id' => (int) $forum_id,
  1245. 'mark_time' => ($post_time) ? (int) $post_time : time(),
  1246. );
  1247. $db->sql_query('INSERT INTO ' . TOPICS_TRACK_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary));
  1248. $db->sql_return_on_error(false);
  1249. }
  1250. }
  1251. else if ($config['load_anon_lastread'] || $user->data['is_registered'])
  1252. {
  1253. $tracking = (isset($_COOKIE[$config['cookie_name'] . '_track'])) ? ((STRIP) ? stripslashes($_COOKIE[$config['cookie_name'] . '_track']) : $_COOKIE[$config['cookie_name'] . '_track']) : '';
  1254. $tracking = ($tracking) ? tracking_unserialize($tracking) : array();
  1255. $topic_id36 = base_convert($topic_id, 10, 36);
  1256. if (!isset($tracking['t'][$topic_id36]))
  1257. {
  1258. $tracking['tf'][$forum_id][$topic_id36] = true;
  1259. }
  1260. $post_time = ($post_time) ? $post_time : time();
  1261. $tracking['t'][$topic_id36] = base_convert($post_time - $config['board_startdate'], 10, 36);
  1262. // If the cookie grows larger than 10000 characters we will remove the smallest value
  1263. // This can result in old topics being unread - but most of the time it should be accurate...
  1264. if (isset($_COOKIE[$config['cookie_name'] . '_track']) && strlen($_COOKIE[$config['cookie_name'] . '_track']) > 10000)
  1265. {
  1266. //echo 'Cookie grown too large' . print_r($tracking, true);
  1267. // We get the ten most minimum stored time offsets and its associated topic ids
  1268. $time_keys = array();
  1269. for ($i = 0; $i < 10 && sizeof($tracking['t']); $i++)
  1270. {
  1271. $min_value = min($tracking['t']);
  1272. $m_tkey = array_search($min_value, $tracking['t']);
  1273. unset($tracking['t'][$m_tkey]);
  1274. $time_keys[$m_tkey] = $min_value;
  1275. }
  1276. // Now remove the topic ids from the array...
  1277. foreach ($tracking['tf'] as $f_id => $topic_id_ary)
  1278. {
  1279. foreach ($time_keys as $m_tkey => $min_value)
  1280. {
  1281. if (isset($topic_id_ary[$m_tkey]))
  1282. {
  1283. $tracking['f'][$f_id] = $min_value;
  1284. unset($tracking['tf'][$f_id][$m_tkey]);
  1285. }
  1286. }
  1287. }
  1288. if ($user->data['is_registered'])
  1289. {
  1290. $user->data['user_lastmark'] = intval(base_convert(max($time_keys) + $config['board_startdate'], 36, 10));
  1291. $db->sql_query('UPDATE ' . USERS_TABLE . ' SET user_lastmark = ' . $user->data['user_lastmark'] . " WHERE user_id = {$user->data['user_id']}");
  1292. }
  1293. else
  1294. {
  1295. $tracking['l'] = max($time_keys);
  1296. }
  1297. }
  1298. $user->set_cookie('track', tracking_serialize($tracking), time() + 31536000);
  1299. $_COOKIE[$config['cookie_name'] . '_track'] = (STRIP) ? addslashes(tracking_serialize($tracking)) : tracking_serialize($tracking);
  1300. }
  1301. return;
  1302. }
  1303. else if ($mode == 'post')
  1304. {
  1305. if ($topic_id === false)
  1306. {
  1307. return;
  1308. }
  1309. $use_user_id = (!$user_id) ? $user->data['user_id'] : $user_id;
  1310. if ($config['load_db_track'] && $use_user_id != ANONYMOUS)
  1311. {
  1312. $db->sql_return_on_error(true);
  1313. $sql_ary = array(
  1314. 'user_id' => (int) $use_user_id,
  1315. 'topic_id' => (int) $topic_id,
  1316. 'topic_posted' => 1
  1317. );
  1318. $db->sql_query('INSERT INTO ' . TOPICS_POSTED_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary));
  1319. $db->sql_return_on_error(false);
  1320. }
  1321. return;
  1322. }
  1323. }
  1324. /**
  1325. * Get topic tracking info by using already fetched info
  1326. */
  1327. function get_topic_tracking($forum_id, $topic_ids, &$rowset, $forum_mark_time, $global_announce_list = false)
  1328. {
  1329. global $config, $user;
  1330. $last_read = array();
  1331. if (!is_array($topic_ids))
  1332. {
  1333. $topic_ids = array($topic_ids);
  1334. }
  1335. foreach ($topic_ids as $topic_id)
  1336. {
  1337. if (!empty($rowset[$topic_id]['mark_time']))
  1338. {
  1339. $last_read[$topic_id] = $rowset[$topic_id]['mark_time'];
  1340. }
  1341. }
  1342. $topic_ids = array_diff($topic_ids, array_keys($last_read));
  1343. if (sizeof($topic_ids))
  1344. {
  1345. $mark_time = array();
  1346. if (!empty($forum_mark_time[$forum_id]) && $forum_mark_time[$forum_id] !== false)
  1347. {
  1348. $mark_time[$forum_id] = $forum_mark_time[$forum_id];
  1349. }
  1350. $user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user->data['user_lastmark'];
  1351. foreach ($topic_ids as $topic_id)
  1352. {
  1353. $last_read[$topic_id] = $user_lastmark;
  1354. }
  1355. }
  1356. return $last_read;
  1357. }
  1358. /**
  1359. * Get topic tracking info from db (for cookie based tracking only this function is used)
  1360. */
  1361. function get_complete_topic_tracking($forum_id, $topic_ids, $global_announce_list = false)
  1362. {
  1363. global $config, $user;
  1364. $last_read = array();
  1365. if (!is_array($topic_ids))
  1366. {
  1367. $topic_ids = array($topic_ids);
  1368. }
  1369. if ($config['load_db_lastread'] && $user->data['is_registered'])
  1370. {
  1371. global $db;
  1372. $sql = 'SELECT topic_id, mark_time
  1373. FROM ' . TOPICS_TRACK_TABLE . "
  1374. WHERE user_id = {$user->data['user_id']}
  1375. AND " . $db->sql_in_set('topic_id', $topic_ids);
  1376. $result = $db->sql_query($sql);
  1377. while ($row = $db->sql_fetchrow($result))
  1378. {
  1379. $last_read[$row['topic_id']] = $row['mark_time'];
  1380. }
  1381. $db->sql_freeresult($result);
  1382. $topic_ids = array_diff($topic_ids, array_keys($last_read));
  1383. if (sizeof($topic_ids))
  1384. {
  1385. $sql = 'SELECT forum_id, mark_time
  1386. FROM ' . FORUMS_TRACK_TABLE . "
  1387. WHERE user_id = {$user->data['user_id']}
  1388. AND forum_id = $forum_id";
  1389. $result = $db->sql_query($sql);
  1390. $mark_time = array();
  1391. while ($row = $db->sql_fetchrow($result))
  1392. {
  1393. $mark_time[$row['forum_id']] = $row['mark_time'];
  1394. }
  1395. $db->sql_freeresult($result);
  1396. $user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user->data['user_lastmark'];
  1397. foreach ($topic_ids as $topic_id)
  1398. {
  1399. $last_read[$topic_id] = $user_lastmark;
  1400. }
  1401. }
  1402. }
  1403. else if ($config['load_anon_lastread'] || $user->data['is_registered'])
  1404. {
  1405. global $tracking_topics;
  1406. if (!isset($tracking_topics) || !sizeof($tracking_topics))
  1407. {
  1408. $tracking_topics = (isset($_COOKIE[$config['cookie_name'] . '_track'])) ? ((STRIP) ? stripslashes($_COOKIE[$config['cookie_name'] . '_track']) : $_COOKIE[$config['cookie_name'] . '_track']) : '';
  1409. $tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
  1410. }
  1411. if (!$user->data['is_registered'])
  1412. {
  1413. $user_lastmark = (isset($tracking_topics['l'])) ? base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate'] : 0;
  1414. }
  1415. else
  1416. {
  1417. $user_lastmark = $user->data['user_lastmark'];
  1418. }
  1419. foreach ($topic_ids as $topic_id)
  1420. {
  1421. $topic_id36 = base_convert($topic_id, 10, 36);
  1422. if (isset($tracking_topics['t'][$topic_id36]))
  1423. {
  1424. $last_read[$topic_id] = base_convert($tracking_topics['t'][$topic_id36], 36, 10) + $config['board_startdate'];
  1425. }
  1426. }
  1427. $topic_ids = array_diff($topic_ids, array_keys($last_read));
  1428. if (sizeof($topic_ids))
  1429. {
  1430. $mark_time = array();
  1431. if ($global_announce_list && sizeof($global_announce_list))
  1432. {
  1433. if (isset($tracking_topics['f'][0]))
  1434. {
  1435. $mark_time[0] = base_convert($tracking_topics['f'][0], 36, 10) + $config['board_startdate'];
  1436. }
  1437. }
  1438. if (isset($tracking_topics['f'][$forum_id]))
  1439. {
  1440. $mark_time[$forum_id] = base_convert($tracking_topics['f'][$forum_id], 36, 10) + $config['board_startdate'];
  1441. }
  1442. $user_lastmark = (isset($mark_time[$forum_id])) ? $mark_time[$forum_id] : $user_lastmark;
  1443. foreach ($topic_ids as $topic_id)
  1444. {
  1445. if ($global_announce_list && isset($global_announce_list[$topic_id]))
  1446. {
  1447. $last_read[$topic_id] = (isset($mark_time[0])) ? $mark_time[0] : $user_lastmark;
  1448. }
  1449. else
  1450. {
  1451. $last_read[$topic_id] = $user_lastmark;
  1452. }
  1453. }
  1454. }
  1455. }
  1456. return $last_read;
  1457. }
  1458. /**
  1459. * Get list of unread topics
  1460. *
  1461. * @param int $user_id User ID (or false for current user)
  1462. * @param string $sql_extra Extra WHERE SQL statement
  1463. * @param string $sql_sort ORDER BY SQL sorting statement
  1464. * @param string $sql_limit Limits the size of unread topics list, 0 for unlimited query
  1465. * @param string $sql_limit_offset Sets the offset of the first row to search, 0 to search from the start
  1466. *
  1467. * @return array[int][int] Topic ids as keys, mark_time of topic as value
  1468. */
  1469. function get_unread_topics($user_id = false, $sql_extra = '', $sql_sort = '', $sql_limit = 1001, $sql_limit_offset = 0)
  1470. {
  1471. global $config, $db, $user;
  1472. $user_id = ($user_id === false) ? (int) $user->data['user_id'] : (int) $user_id;
  1473. // Data array we're going to return
  1474. $unread_topics = array();
  1475. if (empty($sql_sort))
  1476. {
  1477. $sql_sort = 'ORDER BY t.topic_last_post_time DESC';
  1478. }
  1479. if ($config['load_db_lastread'] && $user->data['is_registered'])
  1480. {
  1481. // Get list of the unread topics
  1482. $last_mark = (int) $user->data['user_lastmark'];
  1483. $sql_array = array(
  1484. 'SELECT' => 't.topic_id, t.topic_last_post_time, tt.mark_time as topic_mark_time, ft.mark_time as forum_mark_time',
  1485. 'FROM' => array(TOPICS_TABLE => 't'),
  1486. 'LEFT_JOIN' => array(
  1487. array(
  1488. 'FROM' => array(TOPICS_TRACK_TABLE => 'tt'),
  1489. 'ON' => "tt.user_id = $user_id AND t.topic_id = tt.topic_id",
  1490. ),
  1491. array(
  1492. 'FROM' => array(FORUMS_TRACK_TABLE => 'ft'),
  1493. 'ON' => "ft.user_id = $user_id AND t.forum_id = ft.forum_id",
  1494. ),
  1495. ),
  1496. 'WHERE' => "
  1497. t.topic_last_post_time > $last_mark AND
  1498. (
  1499. (tt.mark_time IS NOT NULL AND t.topic_last_post_time > tt.mark_time) OR
  1500. (tt.mark_time IS NULL AND ft.mark_time IS NOT NULL AND t.topic_last_post_time > ft.mark_time) OR
  1501. (tt.mark_time IS NULL AND ft.mark_time IS NULL)
  1502. )
  1503. $sql_extra
  1504. $sql_sort",
  1505. );
  1506. $sql = $db->sql_build_query('SELECT', $sql_array);
  1507. $result = $db->sql_query_limit($sql, $sql_limit, $sql_limit_offset);
  1508. while ($row = $db->sql_fetchrow($result))
  1509. {
  1510. $topic_id = (int) $row['topic_id'];
  1511. $unread_topics[$topic_id] = ($row['topic_mark_time']) ? (int) $row['topic_mark_time'] : (($row['forum_mark_time']) ? (int) $row['forum_mark_time'] : $last_mark);
  1512. }
  1513. $db->sql_freeresult($result);
  1514. }
  1515. else if ($config['load_anon_lastread'] || $user->data['is_registered'])
  1516. {
  1517. global $tracking_topics;
  1518. if (empty($tracking_topics))
  1519. {
  1520. $tracking_topics = request_var($config['cookie_name'] . '_track', '', false, true);
  1521. $tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
  1522. }
  1523. if (!$user->data['is_registered'])
  1524. {
  1525. $user_lastmark = (isset($tracking_topics['l'])) ? base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate'] : 0;
  1526. }
  1527. else
  1528. {
  1529. $user_lastmark = (int) $user->data['user_lastmark'];
  1530. }
  1531. $sql = 'SELECT t.topic_id, t.forum_id, t.topic_last_post_time
  1532. FROM ' . TOPICS_TABLE . ' t
  1533. WHERE t.topic_last_post_time > ' . $user_lastmark . "
  1534. $sql_extra
  1535. $sql_sort";
  1536. $result = $db->sql_query_limit($sql, $sql_limit, $sql_limit_offset);
  1537. while ($row = $db->sql_fetchrow($result))
  1538. {
  1539. $forum_id = (int) $row['forum_id'];
  1540. $topic_id = (int) $row['topic_id'];
  1541. $topic_id36 = base_convert($topic_id, 10, 36);
  1542. if (isset($tracking_topics['t'][$topic_id36]))
  1543. {
  1544. $last_read = base_convert($tracking_topics['t'][$topic_id36], 36, 10) + $config['board_startdate'];
  1545. if ($row['topic_last_post_time'] > $last_read)
  1546. {
  1547. $unread_topics[$topic_id] = $last_read;
  1548. }
  1549. }
  1550. else if (isset($tracking_topics['f'][$forum_id]))
  1551. {
  1552. $mark_time = base_convert($tracking_topics['f'][$forum_id], 36, 10) + $config['board_startdate'];
  1553. if ($row['topic_last_post_time'] > $mark_time)
  1554. {
  1555. $unread_topics[$topic_id] = $mark_time;
  1556. }
  1557. }
  1558. else
  1559. {
  1560. $unread_topics[$topic_id] = $user_lastmark;
  1561. }
  1562. }
  1563. $db->sql_freeresult($result);
  1564. }
  1565. return $unread_topics;
  1566. }
  1567. /**
  1568. * Check for read forums and update topic tracking info accordingly
  1569. *
  1570. * @param int $forum_id the forum id to check
  1571. * @param int $forum_last_post_time the forums last post time
  1572. * @param int $f_mark_time the forums last mark time if user is registered and load_db_lastread enabled
  1573. * @param int $mark_time_forum false if the mark time needs to be obtained, else the last users forum mark time
  1574. *
  1575. * @return true if complete forum got marked read, else false.
  1576. */
  1577. function update_forum_tracking_info($forum_id, $forum_last_post_time, $f_mark_time = false, $mark_time_forum = false)
  1578. {
  1579. global $db, $tracking_topics, $user, $config, $auth;
  1580. // Determine the users last forum mark time if not given.
  1581. if ($mark_time_forum === false)
  1582. {
  1583. if ($config['load_db_lastread'] && $user->data['is_registered'])
  1584. {
  1585. $mark_time_forum = (!empty($f_mark_time)) ? $f_mark_time : $user->data['user_lastmark'];
  1586. }
  1587. else if ($config['load_anon_lastread'] || $user->data['is_registered'])
  1588. {
  1589. $tracking_topics = (isset($_COOKIE[$config['cookie_name'] . '_track'])) ? ((STRIP) ? stripslashes($_COOKIE[$config['cookie_name'] . '_track']) : $_COOKIE[$config['cookie_name'] . '_track']) : '';
  1590. $tracking_topics = ($tracking_topics) ? tracking_unserialize($tracking_topics) : array();
  1591. if (!$user->data['is_registered'])
  1592. {
  1593. $user->data['user_lastmark'] = (isset($tracking_topics['l'])) ? (int) (base_convert($tracking_topics['l'], 36, 10) + $config['board_startdate']) : 0;
  1594. }
  1595. $mark_time_forum = (isset($tracking_topics['f'][$forum_id])) ? (int) (base_convert($tracking_topics['f'][$forum_id], 36, 10) + $config['board_startdate']) : $user->data['user_lastmark'];
  1596. }
  1597. }
  1598. // Handle update of unapproved topics info.
  1599. // Only update for moderators having m_approve permission for the forum.
  1600. $sql_update_unapproved = ($auth->acl_get('m_approve', $forum_id)) ? '': 'AND t.topic_approved = 1';
  1601. // Check the forum for any left unread topics.
  1602. // If there are none, we mark the forum as read.
  1603. if ($config['load_db_lastread'] && $user->data['is_registered'])
  1604. {
  1605. if ($mark_time_forum >= $forum_last_post_time)
  1606. {
  1607. // We do not need to mark read, this happened before. Therefore setting this to true
  1608. $row = true;
  1609. }
  1610. else
  1611. {
  1612. $sql = 'SELECT t.forum_id FROM ' . TOPICS_TABLE . ' t
  1613. LEFT JOIN ' . TOPICS_TRACK_TABLE . ' tt ON (tt.topic_id = t.topic_id AND tt.user_id = ' . $user->data['user_id'] . ')
  1614. WHERE t.forum_id = ' . $forum_id . '
  1615. AND t.topic_last_post_time > ' . $mark_time_forum . '
  1616. AND t.topic_moved_id = 0 ' .
  1617. $sql_update_unapproved . '
  1618. AND (tt.topic_id IS NULL OR tt.mark_time < t.topic_last_post_time)
  1619. GROUP BY t.forum_id';
  1620. $result = $db->sql_query_limit($sql, 1);
  1621. $row = $db->sql_fetchrow($result);
  1622. $db->sql_freeresult($result);
  1623. }
  1624. }
  1625. else if ($config['load_anon_lastread'] || $user->data['is_registered'])
  1626. {
  1627. // Get information from cookie
  1628. $row = false;
  1629. if (!isset($tracking_topics['tf'][$forum_id]))
  1630. {
  1631. // We do not need to mark read, this happened before. Therefore setting this to true
  1632. $row = true;
  1633. }
  1634. else
  1635. {
  1636. $sql = 'SELECT t.topic_id
  1637. FROM ' . TOPICS_TABLE . ' t
  1638. WHERE t.forum_id = ' . $forum_id . '
  1639. AND t.topic_last_post_time > ' . $mark_time_forum . '
  1640. AND t.topic_moved_id = 0 ' .
  1641. $sql_update_unapproved;
  1642. $result = $db->sql_query($sql);
  1643. $check_forum = $tracking_topics['tf'][$forum_id];
  1644. $unread = false;
  1645. while ($row = $db->sql_fetchrow($result))
  1646. {
  1647. if (!isset($check_forum[base_convert($row['topic_id'], 10, 36)]))
  1648. {
  1649. $unread = true;
  1650. break;
  1651. }
  1652. }
  1653. $db->sql_freeresult($result);
  1654. $row = $unread;
  1655. }
  1656. }
  1657. else
  1658. {
  1659. $row = true;
  1660. }
  1661. if (!$row)
  1662. {
  1663. markread('topics', $forum_id);
  1664. return true;
  1665. }
  1666. return false;
  1667. }
  1668. /**
  1669. * Transform an array into a serialized format
  1670. */
  1671. function tracking_serialize($input)
  1672. {
  1673. $out = '';
  1674. foreach ($input as $key => $value)
  1675. {
  1676. if (is_array($value))
  1677. {
  1678. $out .= $key . ':(' . tracking_serialize($value) . ');';
  1679. }
  1680. else
  1681. {
  1682. $out .= $key . ':' . $value . ';';
  1683. }
  1684. }
  1685. return $out;
  1686. }
  1687. /**
  1688. * Transform a serialized array into an actual array
  1689. */
  1690. function tracking_unserialize($string, $max_depth = 3)
  1691. {
  1692. $n = strlen($string);
  1693. if ($n > 10010)
  1694. {
  1695. die('Invalid data supplied');
  1696. }
  1697. $data = $stack = array();
  1698. $key = '';
  1699. $mode = 0;
  1700. $level = &$data;
  1701. for ($i = 0; $i < $n; ++$i)
  1702. {
  1703. switch ($mode)
  1704. {
  1705. case 0:
  1706. switch ($string[$i])
  1707. {
  1708. case ':':
  1709. $level[$key] = 0;
  1710. $mode = 1;
  1711. break;
  1712. case ')':
  1713. unset($level);
  1714. $level = array_pop($stack);
  1715. $mode = 3;
  1716. break;
  1717. default:
  1718. $key .= $string[$i];
  1719. }
  1720. break;
  1721. case 1:
  1722. switch ($string[$i])
  1723. {
  1724. case '(':
  1725. if (sizeof($stack) >= $max_depth)
  1726. {
  1727. die('Invalid data supplied');
  1728. }
  1729. $stack[] = &$level;
  1730. $level[$key] = array();
  1731. $level = &$level[$key];
  1732. $key = '';
  1733. $mode = 0;
  1734. break;
  1735. default:
  1736. $level[$key] = $string[$i];
  1737. $mode = 2;
  1738. break;
  1739. }
  1740. break;
  1741. case 2:
  1742. switch ($string[$i])
  1743. {
  1744. case ')':
  1745. unset($level);
  1746. $level = array_pop($stack);
  1747. $mode = 3;
  1748. break;
  1749. case ';':
  1750. $key = '';
  1751. $mode = 0;
  1752. break;
  1753. default:
  1754. $level[$key] .= $string[$i];
  1755. break;
  1756. }
  1757. break;
  1758. case 3:
  1759. switch ($string[$i])
  1760. {
  1761. case ')':
  1762. unset($level);
  1763. $level = array_pop($stack);
  1764. break;
  1765. case ';':
  1766. $key = '';
  1767. $mode = 0;
  1768. break;
  1769. default:
  1770. die('Invalid data supplied');
  1771. break;
  1772. }
  1773. break;
  1774. }
  1775. }
  1776. if (sizeof($stack) != 0 || ($mode != 0 && $mode != 3))
  1777. {
  1778. die('Invalid data supplied');
  1779. }
  1780. return $level;
  1781. }
  1782. // Pagination functions
  1783. /**
  1784. * Pagination routine, generates page number sequence
  1785. * tpl_prefix is for using different pagination blocks at one page
  1786. */
  1787. function generate_pagination($base_url, $num_items, $per_page, $start_item, $add_prevnext_text = false, $tpl_prefix = '')
  1788. {
  1789. global $template, $user;
  1790. // Make sure $per_page is a valid value
  1791. $per_page = ($per_page <= 0) ? 1 : $per_page;
  1792. $seperator = '<span class="page-sep">' . $user->lang['COMMA_SEPARATOR'] . '</span>';
  1793. $total_pages = ceil($num_items / $per_page);
  1794. if ($total_pages == 1 || !$num_items)
  1795. {
  1796. return false;
  1797. }
  1798. $on_page = floor($start_item / $per_page) + 1;
  1799. $url_delim = (strpos($base_url, '?') === false) ? '?' : ((strpos($base_url, '?') === strlen($base_url) - 1) ? '' : '&amp;');
  1800. $page_string = ($on_page == 1) ? '<strong>1</strong>' : '<a href="' . $base_url . '">1</a>';
  1801. if ($total_pages > 5)
  1802. {
  1803. $start_cnt = min(max(1, $on_page - 4), $total_pages - 5);
  1804. $end_cnt = max(min($total_pages, $on_page + 4), 6);
  1805. $page_string .= ($start_cnt > 1) ? '<span class="page-dots"> ... </span>' : $seperator;
  1806. for ($i = $start_cnt + 1; $i < $end_cnt; $i++)
  1807. {
  1808. $page_string .= ($i == $on_page) ? '<strong>' . $i . '</strong>' : '<a href="' . $base_url . "{$url_delim}start=" . (($i - 1) * $per_page) . '">' . $i . '</a>';
  1809. if ($i < $end_cnt - 1)
  1810. {
  1811. $page_string .= $seperator;
  1812. }
  1813. }
  1814. $page_string .= ($end_cnt < $total_pages) ? '<span class="page-dots"> ... </span>' : $seperator;
  1815. }
  1816. else
  1817. {
  1818. $page_string .= $seperator;
  1819. for ($i = 2; $i < $total_pages; $i++)
  1820. {
  1821. $page_string .= ($i == $on_page) ? '<strong>' . $i . '</strong>' : '<a href="' . $base_url . "{$url_delim}start=" . (($i - 1) * $per_page) . '">' . $i . '</a>';
  1822. if ($i < $total_pages)
  1823. {
  1824. $page_string .= $seperator;
  1825. }
  1826. }
  1827. }
  1828. $page_string .= ($on_page == $total_pages) ? '<strong>' . $total_pages . '</strong>' : '<a href="' . $base_url . "{$url_delim}start=" . (($total_pages - 1) * $per_page) . '">' . $total_pages . '</a>';
  1829. if ($add_prevnext_text)
  1830. {
  1831. if ($on_page != 1)
  1832. {
  1833. $page_string = '<a href="' . $base_url . "{$url_delim}start=" . (($on_page - 2) * $per_page) . '">' . $user->lang['PREVIOUS'] . '</a>&nbsp;&nbsp;' . $page_string;
  1834. }
  1835. if ($on_page != $total_pages)
  1836. {
  1837. $page_string .= '&nbsp;&nbsp;<a href="' . $base_url . "{$url_delim}start=" . ($on_page * $per_page) . '">' . $user->lang['NEXT'] . '</a>';
  1838. }
  1839. }
  1840. $template->assign_vars(array(
  1841. $tpl_prefix . 'BASE_URL' => $base_url,
  1842. 'A_' . $tpl_prefix . 'BASE_URL' => addslashes($base_url),
  1843. $tpl_prefix . 'PER_PAGE' => $per_page,
  1844. $tpl_prefix . 'PREVIOUS_PAGE' => ($on_page == 1) ? '' : $base_url . "{$url_delim}start=" . (($on_page - 2) * $per_page),
  1845. $tpl_prefix . 'NEXT_PAGE' => ($on_page == $total_pages) ? '' : $base_url . "{$url_delim}start=" . ($on_page * $per_page),
  1846. $tpl_prefix . 'TOTAL_PAGES' => $total_pages,
  1847. ));
  1848. return $page_string;
  1849. }
  1850. /**
  1851. * Return current page (pagination)
  1852. */
  1853. function on_page($num_items, $per_page, $start)
  1854. {
  1855. global $template, $user;
  1856. // Make sure $per_page is a valid value
  1857. $per_page = ($per_page <= 0) ? 1 : $per_page;
  1858. $on_page = floor($start / $per_page) + 1;
  1859. $template->assign_vars(array(
  1860. 'ON_PAGE' => $on_page)
  1861. );
  1862. return sprintf($user->lang['PAGE_OF'], $on_page, max(ceil($num_items / $per_page), 1));
  1863. }
  1864. // Server functions (building urls, redirecting...)
  1865. /**
  1866. * Append session id to url.
  1867. * This function supports hooks.
  1868. *
  1869. * @param string $url The url the session id needs to be appended to (can have params)
  1870. * @param mixed $params String or array of additional url parameters
  1871. * @param bool $is_amp Is url using &amp; (true) or & (false)
  1872. * @param string $session_id Possibility to use a custom session id instead of the global one
  1873. *
  1874. * Examples:
  1875. * <code>
  1876. * append_sid("{$phpbb_root_path}viewtopic.$phpEx?t=1&amp;f=2");
  1877. * append_sid("{$phpbb_root_path}viewtopic.$phpEx", 't=1&amp;f=2');
  1878. * append_sid("{$phpbb_root_path}viewtopic.$phpEx", 't=1&f=2', false);
  1879. * append_sid("{$phpbb_root_path}viewtopic.$phpEx", array('t' => 1, 'f' => 2));
  1880. * </code>
  1881. *
  1882. */
  1883. function append_sid($url, $params = false, $is_amp = true, $session_id = false)
  1884. {
  1885. global $_SID, $_EXTRA_URL, $phpbb_hook, $user;
  1886. if ($params === '' || (is_array($params) && empty($params)))
  1887. {
  1888. // Do not append the ? if the param-list is empty anyway.
  1889. $params = false;
  1890. }
  1891. // Developers using the hook function need to globalise the $_SID and $_EXTRA_URL on their own and also handle it appropriately.
  1892. // They could mimic most of what is within this function
  1893. if (!empty($phpbb_hook) && $phpbb_hook->call_hook(__FUNCTION__, $url, $params, $is_amp, $session_id))
  1894. {
  1895. if ($phpbb_hook->hook_return(__FUNCTION__))
  1896. {
  1897. return $phpbb_hook->hook_return_result(__FUNCTION__);
  1898. }
  1899. }
  1900. $params_is_array = is_array($params);
  1901. // Get anchor
  1902. $anchor = '';
  1903. if (strpos($url, '#') !== false)
  1904. {
  1905. list($url, $anchor) = explode('#', $url, 2);
  1906. $anchor = '#' . $anchor;
  1907. }
  1908. else if (!$params_is_array && strpos($params, '#') !== false)
  1909. {
  1910. list($params, $anchor) = explode('#', $params, 2);
  1911. $anchor = '#' . $anchor;
  1912. }
  1913. // Handle really simple cases quickly
  1914. if ($_SID == '' && $session_id === false && empty($_EXTRA_URL) && !$params_is_array && !$anchor)
  1915. {
  1916. if ($params === false)
  1917. {
  1918. return $url;
  1919. }
  1920. $url_delim = (strpos($url, '?') === false) ? '?' : (($is_amp) ? '&amp;' : '&');
  1921. return $url . ($params !== false ? $url_delim. $params : '');
  1922. }
  1923. // Assign sid if session id is not specified
  1924. if ($session_id === false && $user->data['user_id'] != ANONYMOUS && !$user->data['is_bot'])
  1925. {
  1926. $session_id = $_SID;
  1927. }
  1928. $amp_delim = ($is_amp) ? '&amp;' : '&';
  1929. $url_delim = (strpos($url, '?') === false) ? '?' : $amp_delim;
  1930. // Appending custom url parameter?
  1931. $append_url = (!empty($_EXTRA_URL)) ? implode($amp_delim, $_EXTRA_URL) : '';
  1932. // Use the short variant if possible ;)
  1933. if ($params === false)
  1934. {
  1935. // Append session id
  1936. if (!$session_id)
  1937. {
  1938. return $url . (($append_url) ? $url_delim . $append_url : '') . $anchor;
  1939. }
  1940. else
  1941. {
  1942. return $url . (($append_url) ? $url_delim . $append_url . $amp_delim : $url_delim) . 'sid=' . $session_id . $anchor;
  1943. }
  1944. }
  1945. // Build string if parameters are specified as array
  1946. if (is_array($params))
  1947. {
  1948. $output = array();
  1949. foreach ($params as $key => $item)
  1950. {
  1951. if ($item === NULL)
  1952. {
  1953. continue;
  1954. }
  1955. if ($key == '#')
  1956. {
  1957. $anchor = '#' . $item;
  1958. continue;
  1959. }
  1960. $output[] = $key . '=' . $item;
  1961. }
  1962. $params = implode($amp_delim, $output);
  1963. }
  1964. // Append session id and parameters (even if they are empty)
  1965. // If parameters are empty, the developer can still append his/her parameters without caring about the delimiter
  1966. return $url . (($append_url) ? $url_delim . $append_url . $amp_delim : $url_delim) . $params . ((!$session_id) ? '' : $amp_delim . 'sid=' . $session_id) . $anchor;
  1967. }
  1968. /**
  1969. * Generate board url (example: http://www.example.com/phpBB)
  1970. *
  1971. * @param bool $without_script_path if set to true the script path gets not appended (example: http://www.example.com)
  1972. *
  1973. * @return string the generated board url
  1974. */
  1975. function generate_board_url($without_script_path = false)
  1976. {
  1977. global $config, $user;
  1978. $server_name = $user->host;
  1979. $server_port = (!empty($_SERVER['SERVER_PORT'])) ? (int) $_SERVER['SERVER_PORT'] : (int) getenv('SERVER_PORT');
  1980. // Forcing server vars is the only way to specify/override the protocol
  1981. if ($config['force_server_vars'] || !$server_name)
  1982. {
  1983. $server_protocol = ($config['server_protocol']) ? $config['server_protocol'] : (($config['cookie_secure']) ? 'https://' : 'http://');
  1984. $server_name = $config['server_name'];
  1985. $server_port = (int) $config['server_port'];
  1986. $script_path = $config['script_path'];
  1987. $url = $server_protocol . $server_name;
  1988. $cookie_secure = $config['cookie_secure'];
  1989. }
  1990. else
  1991. {
  1992. // Do not rely on cookie_secure, users seem to think that it means a secured cookie instead of an encrypted connection
  1993. $cookie_secure = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') ? 1 : 0;
  1994. $url = (($cookie_secure) ? 'https://' : 'http://') . $server_name;
  1995. $script_path = $user->page['root_script_path'];
  1996. }
  1997. if ($server_port && (($cookie_secure && $server_port <> 443) || (!$cookie_secure && $server_port <> 80)))
  1998. {
  1999. // HTTP HOST can carry a port number (we fetch $user->host, but for old versions this may be true)
  2000. if (strpos($server_name, ':') === false)
  2001. {
  2002. $url .= ':' . $server_port;
  2003. }
  2004. }
  2005. if (!$without_script_path)
  2006. {
  2007. $url .= $script_path;
  2008. }
  2009. // Strip / from the end
  2010. if (substr($url, -1, 1) == '/')
  2011. {
  2012. $url = substr($url, 0, -1);
  2013. }
  2014. return $url;
  2015. }
  2016. /**
  2017. * Redirects the user to another page then exits the script nicely
  2018. * This function is intended for urls within the board. It's not meant to redirect to cross-domains.
  2019. *
  2020. * @param string $url The url to redirect to
  2021. * @param bool $return If true, do not redirect but return the sanitized URL. Default is no return.
  2022. * @param bool $disable_cd_check If true, redirect() will redirect to an external domain. If false, the redirect point to the boards url if it does not match the current domain. Default is false.
  2023. */
  2024. function redirect($url, $return = false, $disable_cd_check = false)
  2025. {
  2026. global $db, $cache, $config, $user, $phpbb_root_path;
  2027. $failover_flag = false;
  2028. if (empty($user->lang))
  2029. {
  2030. $user->add_lang('common');
  2031. }
  2032. if (!$return)
  2033. {
  2034. garbage_collection();
  2035. }
  2036. // Make sure no &amp;'s are in, this will break the redirect
  2037. $url = str_replace('&amp;', '&', $url);
  2038. // Determine which type of redirect we need to handle...
  2039. $url_parts = @parse_url($url);
  2040. if ($url_parts === false)
  2041. {
  2042. // Malformed url, redirect to current page...
  2043. $url = generate_board_url() . '/' . $user->page['page'];
  2044. }
  2045. else if (!empty($url_parts['scheme']) && !empty($url_parts['host']))
  2046. {
  2047. // Attention: only able to redirect within the same domain if $disable_cd_check is false (yourdomain.com -> www.yourdomain.com will not work)
  2048. if (!$disable_cd_check && $url_parts['host'] !== $user->host)
  2049. {
  2050. $url = generate_board_url();
  2051. }
  2052. }
  2053. else if ($url[0] == '/')
  2054. {
  2055. // Absolute uri, prepend direct url...
  2056. $url = generate_board_url(true) . $url;
  2057. }
  2058. else
  2059. {
  2060. // Relative uri
  2061. $pathinfo = pathinfo($url);
  2062. if (!$disable_cd_check && !file_exists($pathinfo['dirname'] . '/'))
  2063. {
  2064. $url = str_replace('../', '', $url);
  2065. $pathinfo = pathinfo($url);
  2066. if (!file_exists($pathinfo['dirname'] . '/'))
  2067. {
  2068. // fallback to "last known user page"
  2069. // at least this way we know the user does not leave the phpBB root
  2070. $url = generate_board_url() . '/' . $user->page['page'];
  2071. $failover_flag = true;
  2072. }
  2073. }
  2074. if (!$failover_flag)
  2075. {
  2076. // Is the uri pointing to the current directory?
  2077. if ($pathinfo['dirname'] == '.')
  2078. {
  2079. $url = str_replace('./', '', $url);
  2080. // Strip / from the beginning
  2081. if ($url && substr($url, 0, 1) == '/')
  2082. {
  2083. $url = substr($url, 1);
  2084. }
  2085. if ($user->page['page_dir'])
  2086. {
  2087. $url = generate_board_url() . '/' . $user->page['page_dir'] . '/' . $url;
  2088. }
  2089. else
  2090. {
  2091. $url = generate_board_url() . '/' . $url;
  2092. }
  2093. }
  2094. else
  2095. {
  2096. // Used ./ before, but $phpbb_root_path is working better with urls within another root path
  2097. $root_dirs = explode('/', str_replace('\\', '/', phpbb_realpath($phpbb_root_path)));
  2098. $page_dirs = explode('/', str_replace('\\', '/', phpbb_realpath($pathinfo['dirname'])));
  2099. $intersection = array_intersect_assoc($root_dirs, $page_dirs);
  2100. $root_dirs = array_diff_assoc($root_dirs, $intersection);
  2101. $page_dirs = array_diff_assoc($page_dirs, $intersection);
  2102. $dir = str_repeat('../', sizeof($root_dirs)) . implode('/', $page_dirs);
  2103. // Strip / from the end
  2104. if ($dir && substr($dir, -1, 1) == '/')
  2105. {
  2106. $dir = substr($dir, 0, -1);
  2107. }
  2108. // Strip / from the beginning
  2109. if ($dir && substr($dir, 0, 1) == '/')
  2110. {
  2111. $dir = substr($dir, 1);
  2112. }
  2113. $url = str_replace($pathinfo['dirname'] . '/', '', $url);
  2114. // Strip / from the beginning
  2115. if (substr($url, 0, 1) == '/')
  2116. {
  2117. $url = substr($url, 1);
  2118. }
  2119. $url = (!empty($dir) ? $dir . '/' : '') . $url;
  2120. $url = generate_board_url() . '/' . $url;
  2121. }
  2122. }
  2123. }
  2124. // Make sure no linebreaks are there... to prevent http response splitting for PHP < 4.4.2
  2125. if (strpos(urldecode($url), "\n") !== false || strpos(urldecode($url), "\r") !== false || strpos($url, ';') !== false)
  2126. {
  2127. trigger_error('Tried to redirect to potentially insecure url.', E_USER_ERROR);
  2128. }
  2129. // Now, also check the protocol and for a valid url the last time...
  2130. $allowed_protocols = array('http', 'https', 'ftp', 'ftps');
  2131. $url_parts = parse_url($url);
  2132. if ($url_parts === false || empty($url_parts['scheme']) || !in_array($url_parts['scheme'], $allowed_protocols))
  2133. {
  2134. trigger_error('Tried to redirect to potentially insecure url.', E_USER_ERROR);
  2135. }
  2136. if ($return)
  2137. {
  2138. return $url;
  2139. }
  2140. // Redirect via an HTML form for PITA webservers
  2141. if (@preg_match('#Microsoft|WebSTAR|Xitami#', getenv('SERVER_SOFTWARE')))
  2142. {
  2143. header('Refresh: 0; URL='