PageRenderTime 66ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/include/functions.php

https://gitlab.com/LibreTitan/Panther
PHP | 1581 lines | 1062 code | 270 blank | 249 comment | 274 complexity | 09fbdd9c99226d1834a19a55f91f6307 MD5 | raw file
  1. <?php
  2. /**
  3. * Copyright (C) 2015 Panther (https://www.pantherforum.org)
  4. * based on code by FluxBB copyright (C) 2008-2012 FluxBB
  5. * License: http://www.gnu.org/licenses/gpl.html GPL version 3 or higher
  6. */
  7. //
  8. // Cookie stuff!
  9. //
  10. function check_cookie(&$panther_user)
  11. {
  12. global $db, $panther_config;
  13. $now = time();
  14. // If the cookie is set and it matches the correct pattern, then read the values from it
  15. if (isset($_COOKIE[$panther_config['o_cookie_name']]) && preg_match('%^(\d+)\|([0-9a-fA-F]+)\|(\d+)\|([0-9a-fA-F]+)$%', $_COOKIE[$panther_config['o_cookie_name']], $matches))
  16. {
  17. $cookie = array(
  18. 'user_id' => intval($matches[1]),
  19. 'password_hash' => $matches[2],
  20. 'expiration_time' => intval($matches[3]),
  21. 'cookie_hash' => $matches[4],
  22. );
  23. }
  24. // If it has a non-guest user, and hasn't expired
  25. if (isset($cookie) && $cookie['user_id'] > 1 && $cookie['expiration_time'] > $now)
  26. {
  27. // If the cookie has been tampered with
  28. if (!panther_hash_equals(hash_hmac('sha512', $cookie['user_id'].'|'.$cookie['expiration_time'], $panther_config['o_cookie_seed'].'_cookie_hash'), $cookie['cookie_hash']))
  29. {
  30. $expire = $now + 31536000; // The cookie expires after a year
  31. panther_setcookie(1, panther_hash(uniqid(rand(), true)), $expire);
  32. set_default_user();
  33. return;
  34. }
  35. $data = array(
  36. ':id' => $cookie['user_id'],
  37. );
  38. // Check if there's a user with the user ID and password hash from the cookie
  39. $ps = $db->run('SELECT u.*, g.*, o.logged, o.idle FROM '.$db->prefix.'users AS u INNER JOIN '.$db->prefix.'groups AS g ON u.group_id=g.g_id LEFT JOIN '.$db->prefix.'online AS o ON o.user_id=u.id WHERE u.id=:id', $data);
  40. $panther_user = $ps->fetch();
  41. // If user authorisation failed
  42. if (!isset($panther_user['id']) || !panther_hash_equals(hash_hmac('sha512', $panther_user['login_key'], $panther_config['o_cookie_seed'].'_password_hash'), $cookie['password_hash']))
  43. {
  44. $expire = $now + 31536000; // The cookie expires after a year
  45. panther_setcookie(1, panther_hash(uniqid(rand(), true)), $expire);
  46. set_default_user();
  47. return;
  48. }
  49. // Send a new, updated cookie with a new expiration timestamp
  50. $expire = ($cookie['expiration_time'] > $now + $panther_config['o_timeout_visit']) ? $now + 1209600 : $now + $panther_config['o_timeout_visit'];
  51. panther_setcookie($panther_user['id'], $panther_user['login_key'], $expire);
  52. // Set a default language if the user selected language no longer exists
  53. if (!file_exists(PANTHER_ROOT.'lang/'.$panther_user['language']))
  54. $panther_user['language'] = $panther_config['o_default_lang'];
  55. $style_root = (($panther_config['o_style_path'] != 'style') ? $panther_config['o_style_path'] : PANTHER_ROOT.$panther_config['o_style_path']).'/';
  56. // Set a default style if the user selected style no longer exists
  57. if (!file_exists($style_root.$panther_user['style'].'.css'))
  58. $panther_user['style'] = $panther_config['o_default_style'];
  59. if (!$panther_user['disp_topics'])
  60. $panther_user['disp_topics'] = $panther_config['o_disp_topics_default'];
  61. if (!$panther_user['disp_posts'])
  62. $panther_user['disp_posts'] = $panther_config['o_disp_posts_default'];
  63. // Define this if you want this visit to affect the online list and the users last visit data
  64. if (!defined('PANTHER_QUIET_VISIT'))
  65. {
  66. // Update the online list
  67. if (!$panther_user['logged'])
  68. {
  69. $panther_user['logged'] = $now;
  70. $data = array(
  71. ':id' => $panther_user['id'],
  72. ':ident' => $panther_user['username'],
  73. ':logged' => $panther_user['logged'],
  74. );
  75. // REPLACE INTO avoids a user having two rows in the online table
  76. $db->run('REPLACE INTO '.$db->prefix.'online (user_id, ident, logged) VALUES (:id, :ident, :logged)', $data);
  77. // Reset tracked topics
  78. set_tracked_topics(null);
  79. }
  80. else
  81. {
  82. $data = array(
  83. ':id' => $panther_user['id'],
  84. );
  85. // Special case: We've timed out, but no other user has browsed the forums since we timed out
  86. if ($panther_user['logged'] < ($now-$panther_config['o_timeout_visit']))
  87. {
  88. $update = array(
  89. 'last_visit' => $panther_user['logged'],
  90. );
  91. $db->update('users', $update, 'id=:id', $data);
  92. $panther_user['last_visit'] = $panther_user['logged'];
  93. }
  94. $update = array(
  95. 'logged' => $now,
  96. );
  97. if ($panther_user['idle'] == '1')
  98. $update['idle'] = 0;
  99. $db->update('online', $update, 'user_id=:id', $data);
  100. // Update tracked topics with the current expire time
  101. if (isset($_COOKIE[$panther_config['o_cookie_name'].'_track']))
  102. forum_setcookie($panther_config['o_cookie_name'].'_track', $_COOKIE[$panther_config['o_cookie_name'].'_track'], $now + $panther_config['o_timeout_visit']);
  103. }
  104. }
  105. else
  106. {
  107. if (!$panther_user['logged'])
  108. $panther_user['logged'] = $panther_user['last_visit'];
  109. }
  110. $panther_user['is_guest'] = false;
  111. $panther_user['is_admmod'] = $panther_user['g_id'] == PANTHER_ADMIN || $panther_user['g_moderator'] == '1';
  112. $panther_user['is_admin'] = $panther_user['g_id'] == PANTHER_ADMIN || $panther_user['g_moderator'] == '1' && $panther_user['g_admin'] == '1';
  113. $panther_user['is_bot'] = false;
  114. }
  115. else
  116. set_default_user();
  117. }
  118. function panther_hash_equals($hash, $input)
  119. {
  120. if (function_exists('hash_equals'))
  121. return hash_equals((string)$hash, $input);
  122. $input_length = strlen($input);
  123. if ($input_length !== strlen($hash))
  124. return false;
  125. $result = 0;
  126. for ($i = 0; $i < $input_length; $i++)
  127. $result |= ord($input[$i]) ^ ord($hash[$i]);
  128. return $result === 0;
  129. }
  130. //
  131. // Try to determine the current URL
  132. //
  133. function get_current_url($max_length = 0)
  134. {
  135. $protocol = get_current_protocol();
  136. $port = (isset($_SERVER['SERVER_PORT']) && (($_SERVER['SERVER_PORT'] != '80' && $protocol == 'http') || ($_SERVER['SERVER_PORT'] != '443' && $protocol == 'https')) && strpos($_SERVER['HTTP_HOST'], ':') === false) ? ':'.$_SERVER['SERVER_PORT'] : '';
  137. $url = urldecode($protocol.'://'.$_SERVER['HTTP_HOST'].$port.$_SERVER['REQUEST_URI']);
  138. if (strlen($url) <= $max_length || $max_length == 0)
  139. return $url;
  140. // We can't find a short enough url
  141. return null;
  142. }
  143. //
  144. // Fetch the current protocol in use - http or https
  145. //
  146. function get_current_protocol()
  147. {
  148. $protocol = 'http';
  149. // Check if the server is claiming to using HTTPS
  150. if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off')
  151. $protocol = 'https';
  152. // If we are behind a reverse proxy try to decide which protocol it is using
  153. if (defined('FORUM_BEHIND_REVERSE_PROXY'))
  154. {
  155. // Check if we are behind a Microsoft based reverse proxy
  156. if (!empty($_SERVER['HTTP_FRONT_END_HTTPS']) && strtolower($_SERVER['HTTP_FRONT_END_HTTPS']) != 'off')
  157. $protocol = 'https';
  158. // Check if we're behind a "proper" reverse proxy, and what protocol it's using
  159. if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']))
  160. $protocol = strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']);
  161. }
  162. return $protocol;
  163. }
  164. function check_ssl_state()
  165. {
  166. global $panther_config;
  167. if ($panther_config['o_force_ssl'] == '1' && get_current_protocol() == 'http')
  168. {
  169. header('Location: '.str_replace('http://', 'https://', get_current_url()));
  170. exit;
  171. }
  172. }
  173. //
  174. // Fetch the base_url, optionally support HTTPS and HTTP
  175. //
  176. function get_base_url($support_https = true)
  177. {
  178. global $panther_config;
  179. static $base_url;
  180. if (!$support_https)
  181. return $panther_config['o_base_url'];
  182. if (!isset($base_url))
  183. {
  184. // Make sure we are using the correct protocol
  185. $base_url = str_replace(array('http://', 'https://'), get_current_protocol().'://', $panther_config['o_base_url']);
  186. }
  187. return $base_url;
  188. }
  189. //
  190. // Fetch admin IDs
  191. //
  192. function get_admin_ids()
  193. {
  194. if (file_exists(FORUM_CACHE_DIR.'cache_admins.php'))
  195. include FORUM_CACHE_DIR.'cache_admins.php';
  196. if (!defined('PANTHER_ADMINS_LOADED'))
  197. {
  198. if (!defined('FORUM_CACHE_FUNCTIONS_LOADED'))
  199. require PANTHER_ROOT.'include/cache.php';
  200. generate_admins_cache();
  201. require FORUM_CACHE_DIR.'cache_admins.php';
  202. }
  203. return $panther_admins;
  204. }
  205. //
  206. // Fill $panther_user with default values (for guests)
  207. //
  208. function set_default_user()
  209. {
  210. global $db, $panther_user, $panther_config;
  211. $remote_addr = get_remote_address();
  212. $remote_addr = isbotex($remote_addr);
  213. $data = array(
  214. ':ident' => $remote_addr,
  215. );
  216. // Fetch guest user
  217. $ps = $db->run('SELECT u.*, g.*, o.logged, o.last_post, o.last_search FROM '.$db->prefix.'users AS u INNER JOIN '.$db->prefix.'groups AS g ON u.group_id=g.g_id LEFT JOIN '.$db->prefix.'online AS o ON o.ident=:ident WHERE u.id=1', $data);
  218. if (!$ps->rowCount())
  219. error_handler(E_ERROR, 'Unable to fetch guest information. Your database must contain both a guest user and a guest user group.', __FILE__, __LINE__);
  220. $panther_user = $ps->fetch();
  221. // Update online list
  222. if (!$panther_user['logged'])
  223. {
  224. $panther_user['logged'] = time();
  225. $data = array(
  226. ':ident' => $remote_addr,
  227. ':logged' => $panther_user['logged'],
  228. );
  229. // REPLACE INTO avoids a user having two rows in the online table
  230. $db->run('REPLACE INTO '.$db->prefix.'online (user_id, ident, logged) VALUES(1, :ident, :logged)', $data);
  231. }
  232. else
  233. {
  234. $update = array(
  235. 'logged' => time(),
  236. );
  237. $data = array(
  238. ':ident' => $remote_addr,
  239. );
  240. $db->update('online', $update, 'ident=:ident', $data);
  241. }
  242. $panther_user['disp_topics'] = $panther_config['o_disp_topics_default'];
  243. $panther_user['disp_posts'] = $panther_config['o_disp_posts_default'];
  244. $panther_user['timezone'] = $panther_config['o_default_timezone'];
  245. $panther_user['dst'] = $panther_config['o_default_dst'];
  246. $panther_user['language'] = $panther_config['o_default_lang'];
  247. $panther_user['style'] = $panther_config['o_default_style'];
  248. $panther_user['is_guest'] = true;
  249. $panther_user['is_admmod'] = false;
  250. $panther_user['is_admin'] = false;
  251. $panther_user['is_bot'] = (strpos($remote_addr, '[Bot]') !== false);
  252. }
  253. //
  254. // Set a cookie, Panther style!
  255. // Wrapper for forum_setcookie
  256. //
  257. function panther_setcookie($user_id, $password_hash, $expire)
  258. {
  259. global $panther_config;
  260. forum_setcookie($panther_config['o_cookie_name'], $user_id.'|'.hash_hmac('sha512', $password_hash, $panther_config['o_cookie_seed'].'_password_hash').'|'.$expire.'|'.hash_hmac('sha512', $user_id.'|'.$expire, $panther_config['o_cookie_seed'].'_cookie_hash'), $expire);
  261. }
  262. //
  263. // Set a cookie, Panther style!
  264. //
  265. function forum_setcookie($name, $value, $expire)
  266. {
  267. global $panther_config;
  268. if ($expire - time() - $panther_config['o_timeout_visit'] < 1)
  269. $expire = 0;
  270. // Enable sending of a P3P header
  271. header('P3P: CP="CUR ADM"');
  272. setcookie($name, $value, $expire, $panther_config['o_cookie_path'], $panther_config['o_cookie_domain'], $panther_config['o_cookie_secure'], true);
  273. }
  274. //
  275. // Check whether the connecting user is banned (and delete any expired bans while we're at it)
  276. //
  277. function check_bans()
  278. {
  279. global $db, $panther_config, $lang_common, $panther_user, $panther_bans;
  280. // Admins and moderators aren't affected
  281. if ($panther_user['is_admmod'] || !$panther_bans)
  282. return;
  283. // Add a dot or a colon (depending on IPv4/IPv6) at the end of the IP address to prevent banned address
  284. // 192.168.0.5 from matching e.g. 192.168.0.50
  285. $user_ip = get_remote_address();
  286. $user_ip .= (strpos($user_ip, '.') !== false) ? '.' : ':';
  287. $bans_altered = false;
  288. $is_banned = false;
  289. foreach ($panther_bans as $cur_ban)
  290. {
  291. // Has this ban expired?
  292. if ($cur_ban['expire'] != '' && $cur_ban['expire'] <= time())
  293. {
  294. $data = array(
  295. ':id' => $cur_ban['id'],
  296. );
  297. $db->delete('bans', 'id=:id', $data);
  298. $bans_altered = true;
  299. continue;
  300. }
  301. if ($cur_ban['username'] != '' && utf8_strtolower($panther_user['username']) == utf8_strtolower($cur_ban['username']))
  302. $is_banned = true;
  303. if ($cur_ban['ip'] != '')
  304. {
  305. $cur_ban_ips = explode(' ', $cur_ban['ip']);
  306. $num_ips = count($cur_ban_ips);
  307. for ($i = 0; $i < $num_ips; ++$i)
  308. {
  309. // Add the proper ending to the ban
  310. if (strpos($user_ip, '.') !== false)
  311. $cur_ban_ips[$i] = $cur_ban_ips[$i].'.';
  312. else
  313. $cur_ban_ips[$i] = $cur_ban_ips[$i].':';
  314. if (substr($user_ip, 0, strlen($cur_ban_ips[$i])) == $cur_ban_ips[$i])
  315. {
  316. $is_banned = true;
  317. break;
  318. }
  319. }
  320. }
  321. if ($is_banned)
  322. {
  323. $data = array(
  324. ':ident' => $panther_user['username'],
  325. );
  326. $db->delete('online', 'ident=:ident', $data);
  327. message($lang_common['Ban message'].' '.(($cur_ban['expire'] != '') ? $lang_common['Ban message 2'].' '.strtolower(format_time($cur_ban['expire'], true)).'. ' : '').(($cur_ban['message'] != '') ? $lang_common['Ban message 3'].'<br /><br /><strong>'.$cur_ban['message'].'</strong><br /><br />' : '<br /><br />').$lang_common['Ban message 4'].' '.$panther_config['o_admin_email'], true);
  328. }
  329. }
  330. // If we removed any expired bans during our run-through, we need to regenerate the bans cache
  331. if ($bans_altered)
  332. {
  333. if (!defined('FORUM_CACHE_FUNCTIONS_LOADED'))
  334. require PANTHER_ROOT.'include/cache.php';
  335. generate_bans_cache();
  336. }
  337. }
  338. //
  339. // Check username
  340. //
  341. function check_username($username, $exclude_id = null)
  342. {
  343. global $db, $panther_config, $errors, $lang_prof_reg, $lang_register, $lang_common, $panther_bans;
  344. // Include UTF-8 function
  345. require_once PANTHER_ROOT.'include/utf8/strcasecmp.php';
  346. // Convert multiple whitespace characters into one (to prevent people from registering with indistinguishable usernames)
  347. $username = preg_replace('%\s+%s', ' ', $username);
  348. // Validate username
  349. if (panther_strlen($username) < 2)
  350. $errors[] = $lang_prof_reg['Username too short'];
  351. else if (panther_strlen($username) > 25) // This usually doesn't happen since the form element only accepts 25 characters
  352. $errors[] = $lang_prof_reg['Username too long'];
  353. else if (!strcasecmp($username, 'Guest') || !utf8_strcasecmp($username, $lang_common['Guest']))
  354. $errors[] = $lang_prof_reg['Username guest'];
  355. else if (preg_match('%[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}%', $username) || preg_match('%((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))%', $username))
  356. $errors[] = $lang_prof_reg['Username IP'];
  357. else if ((strpos($username, '[') !== false || strpos($username, ']') !== false) && strpos($username, '\'') !== false && strpos($username, '"') !== false)
  358. $errors[] = $lang_prof_reg['Username reserved chars'];
  359. else if (preg_match('%(?:\[/?(?:b|u|s|ins|del|em|i|h|colou?r|quote|code|img|url|email|list|\*|topic|post|forum|user)\]|\[(?:img|url|quote|list)=)%i', $username))
  360. $errors[] = $lang_prof_reg['Username BBCode'];
  361. // Check username for any censored words
  362. if ($panther_config['o_censoring'] == '1' && censor_words($username) != $username)
  363. $errors[] = $lang_register['Username censor'];
  364. $where_cond = '(UPPER(username)=UPPER(:username) OR UPPER(username)=UPPER(:username2)) AND id>1';
  365. $data = array(
  366. ':username' => $username,
  367. ':username2' => ucp_preg_replace('%[^\p{L}\p{N}]%u', '', $username),
  368. );
  369. // Check that the username (or a too similar username) is not already registered
  370. if (!is_null($exclude_id))
  371. {
  372. $where_cond .= ' AND id!=:id';
  373. $data[':id'] = $exclude_id;
  374. }
  375. $ps = $db->select('users', 'username', $data, $where_cond);
  376. if ($ps->rowCount())
  377. {
  378. $busy = $ps->fetchColumn();
  379. $errors[] = $lang_register['Username dupe 1'].' '.$busy.'. '.$lang_register['Username dupe 2'];
  380. }
  381. // Check username for any banned usernames
  382. foreach ($panther_bans as $cur_ban)
  383. {
  384. if ($cur_ban['username'] != '' && utf8_strtolower($username) == utf8_strtolower($cur_ban['username']))
  385. {
  386. $errors[] = $lang_prof_reg['Banned username'];
  387. break;
  388. }
  389. }
  390. }
  391. //
  392. // Update "Users online"
  393. //
  394. function update_users_online()
  395. {
  396. global $db, $panther_config, $panther_user;
  397. $cur_position = substr($_SERVER['REQUEST_URI'], 1);
  398. $server_base = dirname($_SERVER['PHP_SELF']);
  399. if ($server_base !== '/')
  400. $cur_position = substr($cur_position, strlen($server_base));
  401. $cur_position = ($cur_position == '') ? 'index.php' : $cur_position;
  402. $now = time();
  403. $online['users'] = $online['guests'] = array();
  404. // Fetch all online list entries that are older than "o_timeout_online"
  405. $ps = $db->run('SELECT o.user_id, o.ident, o.logged, o.idle, u.group_id FROM '.$db->prefix.'online AS o LEFT JOIN '.$db->prefix.'users AS u ON o.user_id=u.id');
  406. foreach ($ps as $cur_user)
  407. {
  408. if ($cur_user['logged'] < ($now - $panther_config['o_timeout_online']))
  409. {
  410. // If the entry is a guest, delete it
  411. if ($cur_user['user_id'] == '1')
  412. {
  413. $data = array(
  414. ':ident' => $cur_user['ident']
  415. );
  416. $db->delete('online', 'ident=:ident', $data);
  417. }
  418. else
  419. {
  420. // If the entry is older than "o_timeout_visit", update last_visit for the user in question, then delete him/her from the online list
  421. if ($cur_user['logged'] < ($now - $panther_config['o_timeout_visit']))
  422. {
  423. $update = array(
  424. 'last_visit' => $cur_user['logged'],
  425. );
  426. $data = array(
  427. ':id' => $cur_user['user_id'],
  428. );
  429. $db->update('users', $update, 'id=:id', $data);
  430. $db->delete('online', 'user_id=:id', $data);
  431. }
  432. }
  433. }
  434. else
  435. {
  436. if ($cur_user['user_id'] == 1)
  437. $online['guests'][] = array('ident' => $cur_user['ident'], 'group_id' => PANTHER_GUEST);
  438. else
  439. $online['users'][$cur_user['user_id']] = array('username' => $cur_user['ident'], 'group_id' => $cur_user['group_id'], 'id' => $cur_user['user_id']);
  440. }
  441. }
  442. if (!$panther_user['is_bot'])
  443. {
  444. $update = array(
  445. 'currently' => $cur_position,
  446. );
  447. $data = array();
  448. if ($panther_user['is_guest'])
  449. {
  450. $field = 'ident';
  451. $data[':ident'] = get_remote_address();
  452. }
  453. else
  454. {
  455. $field = 'user_id';
  456. $data[':ident'] = $panther_user['id'];
  457. }
  458. $db->update('online', $update, $field.'=:ident', $data);
  459. }
  460. return $online;
  461. }
  462. //
  463. // Display the profile navigation menu
  464. //
  465. function generate_profile_menu($page = '')
  466. {
  467. global $lang_profile, $panther_config, $panther_user, $id, $panther_url;
  468. $sections = array(
  469. array('page' => 'essentials', 'link' => panther_link($panther_url['profile_essentials'], array($id)), 'lang' => $lang_profile['Section essentials']),
  470. array('page' => 'personal', 'link' => panther_link($panther_url['profile_personal'], array($id)), 'lang' => $lang_profile['Section personal']),
  471. array('page' => 'messaging', 'link' => panther_link($panther_url['profile_messaging'], array($id)), 'lang' => $lang_profile['Section messaging']),
  472. );
  473. if ($panther_config['o_avatars'] == '1' || $panther_config['o_signatures'] == '1')
  474. $sections[] = array('page' => 'personality', 'link' => panther_link($panther_url['profile_personality'], array($id)), 'lang' => $lang_profile['Section personality']);
  475. $sections[] = array('page' => 'display', 'link' => panther_link($panther_url['profile_display'], array($id)), 'lang' => $lang_profile['Section display']);
  476. $sections[] = array('page' => 'privacy', 'link' => panther_link($panther_url['profile_privacy'], array($id)), 'lang' => $lang_profile['Section privacy']);
  477. $sections[] = array('page' => 'view', 'link' => panther_link($panther_url['profile_view'], array($id)), 'lang' => $lang_profile['Section view']);
  478. if ($panther_user['is_admin'] || ($panther_user['g_moderator'] == '1' && $panther_user['g_mod_ban_users'] == '1'))
  479. $sections[] = array('page' => 'admin', 'link' => panther_link($panther_url['profile_admin'], array($id)), 'lang' => $lang_profile['Section admin']);
  480. $tpl = load_template('profile_sidebar.tpl');
  481. echo $tpl->render(
  482. array(
  483. 'lang_profile' => $lang_profile,
  484. 'sections' => $sections,
  485. 'page' => $page,
  486. )
  487. );
  488. }
  489. //
  490. // Display PM menu
  491. //
  492. function generate_pm_menu($page = 2) // Default to inbox
  493. {
  494. global $panther_url, $lang_pm, $panther_user, $db;
  495. static $folders;
  496. $percent = ($panther_user['g_pm_limit'] != '0') ? round(ceil($panther_user['num_pms'] / $panther_user['g_pm_limit']*100), 0) : 0;
  497. $limit = ($panther_user['g_pm_limit'] == '0') ? '&infin;' : $panther_user['g_pm_limit'];
  498. $data = array(
  499. ':uid' => $panther_user['id'],
  500. );
  501. $folders = array();
  502. $ps = $db->select('folders', 'name, id', $data, 'user_id=:uid OR user_id=1', 'id, user_id ASC');
  503. foreach ($ps as $folder)
  504. {
  505. $data = array(
  506. ':id' => $folder['id'],
  507. ':uid' => $panther_user['id'],
  508. );
  509. $ps1 = $db->select('pms_data', 'COUNT(topic_id)', $data, 'user_id=:uid AND deleted=0 AND (folder_id=:id '.(($folder['id'] == 1) ? 'OR viewed=0)' : ')'));
  510. $amount = $ps1->fetchColumn();
  511. $folders[] = array(
  512. 'id' => $folder['id'],
  513. 'link' => panther_link($panther_url['box'], array($folder['id'])),
  514. 'name' => $folder['name'],
  515. 'amount' => $amount,
  516. );
  517. }
  518. $tpl = load_template('pm_sidebar.tpl');
  519. return $tpl->render(
  520. array(
  521. 'lang_pm' => $lang_pm,
  522. 'folders' => $folders,
  523. 'percent' => $percent,
  524. 'num_pms' => forum_number_format($panther_user['num_pms']),
  525. 'limit' => forum_number_format($limit),
  526. 'blocked_link' => panther_link($panther_url['pms_blocked']),
  527. 'folders_link' => panther_link($panther_url['pms_folders']),
  528. 'page' => $page,
  529. )
  530. );
  531. }
  532. //
  533. // Outputs markup to display a user's avatar
  534. //
  535. function generate_avatar_markup($user_id, $user_email, $use_gravatar = 0, $size = array())
  536. {
  537. global $panther_config;
  538. static $user_avatar_cache = array();
  539. $avatar_path = ($panther_config['o_avatars_dir'] != '') ? $panther_config['o_avatars_path'] : PANTHER_ROOT.$panther_config['o_avatars_path'];
  540. $avatar_dir = ($panther_config['o_avatars_dir'] != '') ? $panther_config['o_avatars_dir'] : get_base_url(true).'/'.$panther_config['o_avatars_path'];
  541. if (!isset($user_avatar_cache[$user_id]))
  542. {
  543. if ($use_gravatar == 1)
  544. {
  545. $params = (count($size) == 2) ? array($size[0], $size[1]) : array($panther_config['o_avatars_width'], $panther_config['o_avatars_height']);
  546. $user_avatar_cache[$user_id] = '<img src="https://www.gravatar.com/avatar.php?gravatar_id='.md5(strtolower($user_email)).'&amp;size='.$params[0].'" width="'.$params[0].'" height="'.$params[1].'" alt="" />';
  547. }
  548. else if ($panther_config['o_avatar_upload'] == '1')
  549. {
  550. $filetypes = array('jpg', 'gif', 'png');
  551. foreach ($filetypes as $cur_type)
  552. {
  553. $path = $avatar_path.$user_id.'.'.$cur_type;
  554. $url = $avatar_dir.$user_id.'.'.$cur_type;
  555. if (file_exists($path) && $img_size = getimagesize($path))
  556. {
  557. $size = (count($size) == 2 ? 'width="'.$size[0].'" height="'.$size[1].'"' : $img_size[3]);
  558. $user_avatar_cache[$user_id] = '<img src="'.$url.'?m='.filemtime($path).'" '.$size.' alt="" />';
  559. break;
  560. }
  561. }
  562. }
  563. // If there's no avatar set, we mustn't have one uploaded. Set the default!
  564. if (!isset($user_avatar_cache[$user_id]))
  565. {
  566. $path = $avatar_path.'1.'.$panther_config['o_avatar'];
  567. $url = $avatar_dir.'1.'.$panther_config['o_avatar'];
  568. $img_size = getimagesize($path);
  569. $size = (count($size) == 2 ? 'width="'.$size[0].'" height="'.$size[1].'"' : $img_size[3]);
  570. $user_avatar_cache[$user_id] = '<img src="'.$url.'?m='.filemtime($path).'" '.$size.' alt="" />';
  571. }
  572. }
  573. return $user_avatar_cache[$user_id];
  574. }
  575. //
  576. // Generate browser's title
  577. //
  578. function generate_page_title($page_title, $p = null)
  579. {
  580. global $lang_common;
  581. if (!is_array($page_title))
  582. $page_title = array($page_title);
  583. $page_title = array_reverse($page_title);
  584. if ($p > 1)
  585. $page_title[0] .= ' ('.sprintf($lang_common['Page'], forum_number_format($p)).')';
  586. $crumbs = implode($lang_common['Title separator'], $page_title);
  587. return $crumbs;
  588. }
  589. //
  590. // Save array of tracked topics in cookie
  591. //
  592. function set_tracked_topics($tracked_topics)
  593. {
  594. global $panther_config;
  595. $cookie_data = '';
  596. if (!empty($tracked_topics))
  597. {
  598. // Sort the arrays (latest read first)
  599. arsort($tracked_topics['topics'], SORT_NUMERIC);
  600. arsort($tracked_topics['forums'], SORT_NUMERIC);
  601. // Homebrew serialization (to avoid having to run unserialize() on cookie data)
  602. foreach ($tracked_topics['topics'] as $id => $timestamp)
  603. $cookie_data .= 't'.$id.'='.$timestamp.';';
  604. foreach ($tracked_topics['forums'] as $id => $timestamp)
  605. $cookie_data .= 'f'.$id.'='.$timestamp.';';
  606. // Enforce a byte size limit (4096 minus some space for the cookie name - defaults to 4048)
  607. if (strlen($cookie_data) > FORUM_MAX_COOKIE_SIZE)
  608. {
  609. $cookie_data = substr($cookie_data, 0, FORUM_MAX_COOKIE_SIZE);
  610. $cookie_data = substr($cookie_data, 0, strrpos($cookie_data, ';')).';';
  611. }
  612. }
  613. forum_setcookie($panther_config['o_cookie_name'].'_track', $cookie_data, time() + $panther_config['o_timeout_visit']);
  614. $_COOKIE[$panther_config['o_cookie_name'].'_track'] = $cookie_data; // Set it directly in $_COOKIE as well
  615. }
  616. //
  617. // Extract array of tracked topics from cookie
  618. //
  619. function get_tracked_topics()
  620. {
  621. global $panther_config;
  622. $cookie_data = isset($_COOKIE[$panther_config['o_cookie_name'].'_track']) ? $_COOKIE[$panther_config['o_cookie_name'].'_track'] : false;
  623. if (!$cookie_data)
  624. return array('topics' => array(), 'forums' => array());
  625. if (strlen($cookie_data) > FORUM_MAX_COOKIE_SIZE)
  626. return array('topics' => array(), 'forums' => array());
  627. // Unserialize data from cookie
  628. $tracked_topics = array('topics' => array(), 'forums' => array());
  629. $temp = explode(';', $cookie_data);
  630. foreach ($temp as $t)
  631. {
  632. $type = substr($t, 0, 1) == 'f' ? 'forums' : 'topics';
  633. $id = intval(substr($t, 1));
  634. $timestamp = intval(substr($t, strpos($t, '=') + 1));
  635. if ($id > 0 && $timestamp > 0)
  636. $tracked_topics[$type][$id] = $timestamp;
  637. }
  638. return $tracked_topics;
  639. }
  640. //
  641. // Update posts, topics, last_post, last_post_id and last_poster for a forum
  642. //
  643. function update_forum($forum_id)
  644. {
  645. global $db;
  646. $data = array(
  647. ':id' => $forum_id,
  648. );
  649. $ps = $db->select('topics', 'COUNT(id), SUM(num_replies)', $data, 'forum_id=:id AND approved=1 AND deleted=0');
  650. list($num_topics, $num_posts) = $ps->fetch(PDO::FETCH_NUM);
  651. $num_posts = $num_posts + $num_topics; // $num_posts is only the sum of all replies (we have to add the topic posts)
  652. $data = array(
  653. ':id' => $forum_id
  654. );
  655. $ps = $db->select('topics', 'last_post, last_post_id, last_poster, subject, id', $data, 'forum_id=:id AND approved=1 AND deleted=0 AND moved_to IS NULL ORDER BY last_post DESC LIMIT 1');
  656. if ($ps->rowCount()) // There are topics in the forum
  657. {
  658. list($last_post, $last_post_id, $last_poster, $last_topic, $last_topic_id) = $ps->fetch(PDO::FETCH_NUM);
  659. $update = array(
  660. 'num_topics' => $num_topics,
  661. 'num_posts' => $num_posts,
  662. 'last_post' => $last_post,
  663. 'last_post_id' => $last_post_id,
  664. 'last_topic' => $last_topic,
  665. 'last_topic_id' => $last_topic_id,
  666. 'last_poster' => $last_poster,
  667. );
  668. $data = array(
  669. ':id' => $forum_id,
  670. );
  671. $db->update('forums', $update, 'id=:id', $data);
  672. }
  673. else // There are no topics
  674. {
  675. $data = array(
  676. ':num_topics' => $num_topics,
  677. ':num_posts' => $num_posts,
  678. ':id' => $forum_id,
  679. );
  680. // Annoyingly PDO does not allow NULL values to be added in prepared statements. When added it becomes 'NULL', so we have to run the query manually instead.
  681. $db->run('UPDATE '.$db->prefix.'forums SET num_topics=:num_topics, num_posts=:num_posts, last_post=NULL, last_post_id=NULL, last_poster=NULL, last_topic=\'\', last_topic_id=0 WHERE id=:id', $data);
  682. }
  683. }
  684. //
  685. // Deletes any avatars owned by the specified user ID
  686. //
  687. function delete_avatar($user_id)
  688. {
  689. global $panther_config;
  690. $filetypes = array('jpg', 'gif', 'png');
  691. $avatar_path = ($panther_config['o_avatars_dir'] != '') ? $panther_config['o_avatars_path'] : PANTHER_ROOT.$panther_config['o_avatars_path'];
  692. // Delete user avatar
  693. foreach ($filetypes as $cur_type)
  694. {
  695. if (file_exists($avatar_path.$user_id.'.'.$cur_type))
  696. @unlink($avatar_path.$user_id.'.'.$cur_type);
  697. }
  698. }
  699. //
  700. // Delete a topic and all of its posts
  701. //
  702. function delete_topic($topic_id)
  703. {
  704. global $db, $panther_config;
  705. // Delete the topic and any redirect topics
  706. attach_delete_thread($topic_id);
  707. $data = array(
  708. ':id' => $topic_id,
  709. );
  710. $topic = array(
  711. ':id' => $topic_id,
  712. ':moved_to' => $topic_id,
  713. );
  714. $update = array(
  715. 'deleted' => 1,
  716. );
  717. $post_ids = array();
  718. $db->update('topics', $update, 'id=:id OR moved_to=:moved_to', $topic);
  719. $db->delete('polls', 'topic_id=:id', $data);
  720. // Get all post IDs from this topic.
  721. $ps = $db->select('posts', 'id', $data, 'topic_id=:id');
  722. foreach ($ps as $cur_post)
  723. $post_ids[] = $cur_post['id'];
  724. // Make sure we have a list of post IDs
  725. if (!empty($post_ids))
  726. {
  727. strip_search_index($post_ids); // Should be an array
  728. $db->update('posts', $update, 'topic_id=:id', $data);
  729. }
  730. if ($panther_config['o_delete_full'] == '1')
  731. permanently_delete_topic($topic_id);
  732. }
  733. //
  734. // Delete a single post
  735. //
  736. function delete_post($post_id, $topic_id)
  737. {
  738. global $db, $panther_config;
  739. $topic_data = array(
  740. ':id' => $topic_id,
  741. );
  742. $post_data = array(
  743. ':id' => $post_id,
  744. );
  745. $ps = $db->select('posts', 'id, poster, posted', $topic_data, 'topic_id=:id AND approved=1 AND deleted=0', 'id DESC LIMIT 2');
  746. list($last_id, ,) = $ps->fetch(PDO::FETCH_NUM);
  747. list($second_last_id, $second_poster, $second_posted) = $ps->fetch(PDO::FETCH_NUM);
  748. // Delete the post
  749. attach_delete_post($post_id);
  750. $update = array(
  751. 'deleted' => 1,
  752. );
  753. $db->update('posts', $update, 'id=:id', $post_data);
  754. strip_search_index(array($post_id));
  755. // Count number of replies in the topic
  756. $ps = $db->select('posts', 'COUNT(id)', $topic_data, 'topic_id=:id AND approved=1 AND deleted=0');
  757. $num_replies = $ps->fetchColumn() - 1; // Decrement the deleted post
  758. // If the message we deleted is the most recent in the topic (at the end of the topic)
  759. if ($last_id == $post_id)
  760. {
  761. // If there is a $second_last_id there is more than 1 reply to the topic
  762. if (!empty($second_last_id))
  763. {
  764. $update = array(
  765. 'last_post' => $second_posted,
  766. 'last_post_id' => $second_last_id,
  767. 'last_poster' => $second_poster,
  768. 'num_replies' => $num_replies,
  769. );
  770. $data = array(
  771. ':id' => $topic_id,
  772. );
  773. $db->update('topics', $update, 'id=:id', $data);
  774. }
  775. else
  776. {
  777. $data = array(
  778. ':id' => $topic_id,
  779. ':num_replies' => $num_replies-1,
  780. );
  781. // We deleted the only reply, so now last_post/last_post_id/last_poster is posted/id/poster from the topic itself
  782. $db->run('UPDATE '.$db->prefix.'topics SET last_post=posted, last_post_id=id, last_poster=poster, num_replies=:num_replies WHERE id=:id', $data);
  783. }
  784. }
  785. else // Otherwise we just decrement the reply counter
  786. {
  787. $update = array(
  788. 'num_replies' => $num_replies,
  789. );
  790. $db->update('topics', $update, 'id=:id', $topic_data);
  791. }
  792. if ($panther_config['o_delete_full'] == '1')
  793. permanently_delete_post($post_id);
  794. }
  795. //
  796. // Permanently delete a single post
  797. //
  798. function permanently_delete_post($id)
  799. {
  800. global $db;
  801. $data = array(
  802. ':id' => $id,
  803. );
  804. $db->delete('posts', 'id=:id AND deleted=1', $data); // Since we've already stripped the search index, all we need to do is delete the row
  805. }
  806. //
  807. // Permanently delete a topic
  808. //
  809. function permanently_delete_topic($id)
  810. {
  811. global $db;
  812. $data = array(
  813. ':id' => $id,
  814. ':moved_to' => $id,
  815. );
  816. $db->delete('topics', '(id=:id OR moved_to=:moved_to) AND deleted=1', $data);
  817. unset($data[':moved_to']);
  818. $db->delete('posts', 'topic_id=? AND deleted=1', array_values($data));
  819. // Delete any subscriptions for this topic
  820. $db->delete('topic_subscriptions', 'topic_id=?', array_values($data));
  821. }
  822. //
  823. // Delete every .php file in the forum's cache directory
  824. //
  825. function forum_clear_cache()
  826. {
  827. $files = array_diff(scandir(FORUM_CACHE_DIR), array('.', '..'));
  828. foreach ($files as $file)
  829. {
  830. if (substr($file, -4) == '.php')
  831. @unlink(FORUM_CACHE_DIR.$file);
  832. }
  833. }
  834. //
  835. // Replace censored words in $text
  836. //
  837. function censor_words($text)
  838. {
  839. global $db;
  840. static $search_for, $replace_with;
  841. // If not already built in a previous call, build an array of censor words and their replacement text
  842. if (!isset($search_for))
  843. {
  844. if (file_exists(FORUM_CACHE_DIR.'cache_censoring.php'))
  845. include FORUM_CACHE_DIR.'cache_censoring.php';
  846. if (!defined('PANTHER_CENSOR_LOADED'))
  847. {
  848. if (!defined('FORUM_CACHE_FUNCTIONS_LOADED'))
  849. require PANTHER_ROOT.'include/cache.php';
  850. generate_censoring_cache();
  851. require FORUM_CACHE_DIR.'cache_censoring.php';
  852. }
  853. }
  854. if (!empty($search_for))
  855. $text = substr(ucp_preg_replace($search_for, $replace_with, ' '.$text.' '), 1, -1);
  856. return $text;
  857. }
  858. //
  859. // Determines the correct title for $user
  860. // $user must contain the elements 'username', 'title', 'posts', 'g_id' and 'g_user_title'
  861. //
  862. function get_title($user)
  863. {
  864. global $panther_bans, $lang_common, $panther_config;
  865. static $ban_list, $panther_ranks;
  866. // If not already built in a previous call, build an array of lowercase banned usernames
  867. if (empty($ban_list))
  868. {
  869. $ban_list = array();
  870. foreach ($panther_bans as $cur_ban)
  871. $ban_list[] = utf8_strtolower($cur_ban['username']);
  872. }
  873. // If not already loaded in a previous call, load the cached ranks
  874. if ($panther_config['o_ranks'] == '1' && !defined('PANTHER_RANKS_LOADED'))
  875. {
  876. if (file_exists(FORUM_CACHE_DIR.'cache_ranks.php'))
  877. include FORUM_CACHE_DIR.'cache_ranks.php';
  878. if (!defined('PANTHER_RANKS_LOADED'))
  879. {
  880. if (!defined('FORUM_CACHE_FUNCTIONS_LOADED'))
  881. require PANTHER_ROOT.'include/cache.php';
  882. generate_ranks_cache();
  883. require FORUM_CACHE_DIR.'cache_ranks.php';
  884. }
  885. }
  886. // If the user has a custom title
  887. if ($user['title'] != '')
  888. $user_title = $user['title'];
  889. // If the user is banned
  890. else if (in_array(utf8_strtolower($user['username']), $ban_list))
  891. $user_title = $lang_common['Banned'];
  892. // If the user group has a default user title
  893. else if ($user['g_user_title'] != '')
  894. $user_title = $user['g_user_title'];
  895. // If the user is a guest
  896. else if ($user['g_id'] == PANTHER_GUEST)
  897. $user_title = $lang_common['Guest'];
  898. // If nothing else helps, we assign the default
  899. else
  900. {
  901. // Are there any ranks?
  902. if ($panther_config['o_ranks'] == '1' && !empty($panther_ranks))
  903. {
  904. foreach ($panther_ranks as $cur_rank)
  905. {
  906. if ($user['num_posts'] >= $cur_rank['min_posts'])
  907. $user_title = $cur_rank['rank'];
  908. }
  909. }
  910. // If the user didn't "reach" any rank (or if ranks are disabled), we assign the default
  911. if (!isset($user_title))
  912. $user_title = $lang_common['Member'];
  913. }
  914. return $user_title;
  915. }
  916. //
  917. // Generate a string with numbered links (for multipage scripts)
  918. //
  919. function paginate($num_pages, $cur_page, $link, $args = null)
  920. {
  921. global $lang_common, $panther_config, $panther_url;
  922. $pages = array();
  923. $link_to_all = false;
  924. // If $cur_page == -1, we link to all pages (used in viewforum.php)
  925. if ($cur_page == -1)
  926. {
  927. $cur_page = 1;
  928. $link_to_all = true;
  929. }
  930. if ($num_pages > 1)
  931. {
  932. if ($cur_page > 1)
  933. $pages[] = array('item' => true, 'href' => str_replace('#', '', get_sublink($link, $panther_url['page'], ($cur_page - 1), $args)), 'current' => $lang_common['Previous']);
  934. if ($cur_page > 3)
  935. {
  936. $pages[] = array('item' => (empty($pages) ? true : false), 'href' => $link, 'current' => 1);
  937. if ($cur_page > 5)
  938. $pages[] = $lang_common['Spacer'];
  939. }
  940. // Don't ask me how the following works. It just does, OK? =)
  941. for ($current = ($cur_page == 5) ? $cur_page - 3 : $cur_page - 2, $stop = ($cur_page + 4 == $num_pages) ? $cur_page + 4 : $cur_page + 3; $current < $stop; ++$current)
  942. {
  943. if ($current < 1 || $current > $num_pages)
  944. continue;
  945. else if ($current != $cur_page || $link_to_all)
  946. $pages[] = array('item' => (empty($pages) ? true : false), 'href' => str_replace('#', '', get_sublink($link, $panther_url['page'], $current, $args)), 'current' => forum_number_format($current));
  947. else
  948. $pages[] = array('item' => (empty($pages) ? true : false), 'current' => forum_number_format($current));
  949. }
  950. if ($cur_page <= ($num_pages-3))
  951. {
  952. if ($cur_page != ($num_pages-3) && $cur_page != ($num_pages-4))
  953. $pages[] = $lang_common['Spacer'];
  954. $pages[] = array('item' => (empty($pages) ? true : false), 'href' => get_sublink($link, $panther_url['page'], $num_pages, $args), 'current' => forum_number_format($num_pages));
  955. }
  956. // Add a next page link
  957. if ($num_pages > 1 && !$link_to_all && $cur_page < $num_pages)
  958. $pages[] = array('item' => (empty($pages) ? true : false), 'rel' => 'next', 'href' => get_sublink($link, $panther_url['page'], ($cur_page + 1), $args), 'current' => $lang_common['Next']);
  959. }
  960. $tpl = load_template('pagination.tpl');
  961. return $tpl->render(
  962. array(
  963. 'num_pages' => $num_pages,
  964. 'cur_page' => $cur_page,
  965. 'pages' => $pages,
  966. 'link' => $link,
  967. )
  968. );
  969. }
  970. //
  971. // Display a message
  972. //
  973. function message($message, $no_back_link = false, $http_status = null)
  974. {
  975. global $db, $lang_common, $panther_config, $panther_start, $tpl_main, $panther_user, $panther_url;
  976. // Did we receive a custom header?
  977. if (!is_null($http_status))
  978. header('HTTP/1.1 '.$http_status);
  979. if (!defined('PANTHER_HEADER'))
  980. {
  981. $page_title = array($panther_config['o_board_title'], $lang_common['Info']);
  982. define('PANTHER_ACTIVE_PAGE', 'index');
  983. require PANTHER_ROOT.'header.php';
  984. }
  985. $tpl = load_template('message.tpl');
  986. echo $tpl->render(
  987. array(
  988. 'lang_common' => $lang_common,
  989. 'message' => $message,
  990. 'no_back_link' => $no_back_link,
  991. )
  992. );
  993. require PANTHER_ROOT.'footer.php';
  994. }
  995. //
  996. // Format a time string according to $time_format and time zones
  997. //
  998. function format_time($timestamp, $date_only = false, $date_format = null, $time_format = null, $time_only = false, $no_text = false)
  999. {
  1000. global $lang_common, $panther_user, $forum_date_formats, $forum_time_formats;
  1001. if ($timestamp == '')
  1002. return $lang_common['Never'];
  1003. $diff = ($panther_user['timezone'] + $panther_user['dst']) * 3600;
  1004. $timestamp += $diff;
  1005. $now = time();
  1006. if(is_null($date_format))
  1007. $date_format = $forum_date_formats[$panther_user['date_format']];
  1008. if(is_null($time_format))
  1009. $time_format = $forum_time_formats[$panther_user['time_format']];
  1010. $date = gmdate($date_format, $timestamp);
  1011. $today = gmdate($date_format, $now+$diff);
  1012. $yesterday = gmdate($date_format, $now+$diff-86400);
  1013. if (!$no_text)
  1014. {
  1015. if ($date == $today)
  1016. $date = $lang_common['Today'];
  1017. else if ($date == $yesterday)
  1018. $date = $lang_common['Yesterday'];
  1019. }
  1020. if ($date_only)
  1021. return $date;
  1022. else if ($time_only)
  1023. return gmdate($time_format, $timestamp);
  1024. else
  1025. return $date.' '.gmdate($time_format, $timestamp);
  1026. }
  1027. //
  1028. // A wrapper for PHP's number_format function
  1029. //
  1030. function forum_number_format($number, $decimals = 0)
  1031. {
  1032. global $lang_common;
  1033. return is_numeric($number) ? number_format($number, $decimals, $lang_common['lang_decimal_point'], $lang_common['lang_thousands_sep']) : $number;
  1034. }
  1035. //
  1036. // Generate a random key of length $len
  1037. //
  1038. function random_key($len, $readable = false, $hash = false)
  1039. {
  1040. if (!function_exists('secure_random_bytes'))
  1041. include PANTHER_ROOT.'include/srand.php';
  1042. $key = secure_random_bytes($len);
  1043. if ($hash)
  1044. return substr(bin2hex($key), 0, $len);
  1045. else if ($readable)
  1046. {
  1047. $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  1048. $result = '';
  1049. for ($i = 0; $i < $len; ++$i)
  1050. $result .= substr($chars, (ord($key[$i]) % strlen($chars)), 1);
  1051. return $result;
  1052. }
  1053. return $key;
  1054. }
  1055. //
  1056. // Make sure that user is using a valid token
  1057. //
  1058. function confirm_referrer($script, $use_ip = true)
  1059. {
  1060. global $lang_common, $panther_user;
  1061. // Yeah, pretty complex ternary =)
  1062. $sent_hash = ((isset($_POST['csrf_token'])) ? panther_trim($_POST['csrf_token']) : (isset($_GET['csrf_token']) ? panther_trim($_GET['csrf_token']) : ''));
  1063. if (!panther_hash_equals(generate_csrf_token($script, $use_ip), $sent_hash))
  1064. message($lang_common['Bad referrer']);
  1065. }
  1066. //
  1067. // Generate a csrf token
  1068. //
  1069. function generate_csrf_token($script = 'nothing', $use_ip = true)
  1070. {
  1071. global $panther_user;
  1072. $script = ($script != 'nothing') ? $script : panther_trim(basename($_SERVER['SCRIPT_NAME']));
  1073. return panther_hash($panther_user['id'].$script.$panther_user['salt'].(($use_ip ? get_remote_address() : '')).$panther_user['login_key']);
  1074. }
  1075. //
  1076. // Validate the given redirect URL, use the fallback otherwise
  1077. //
  1078. function validate_redirect($redirect_url, $fallback_url)
  1079. {
  1080. $referrer = parse_url(strtolower($redirect_url));
  1081. // Make sure the host component exists
  1082. if (!isset($referrer['host']))
  1083. $referrer['host'] = '';
  1084. // Remove www subdomain if it exists
  1085. if (strpos($referrer['host'], 'www.') === 0)
  1086. $referrer['host'] = substr($referrer['host'], 4);
  1087. // Make sure the path component exists
  1088. if (!isset($referrer['path']))
  1089. $referrer['path'] = '';
  1090. $valid = parse_url(strtolower(get_base_url()));
  1091. // Remove www subdomain if it exists
  1092. if (strpos($valid['host'], 'www.') === 0)
  1093. $valid['host'] = substr($valid['host'], 4);
  1094. // Make sure the path component exists
  1095. if (!isset($valid['path']))
  1096. $valid['path'] = '';
  1097. if ($referrer['host'] == $valid['host'] && preg_match('%^'.preg_quote($valid['path'], '%').'/(.*?)\.php%i', $referrer['path']))
  1098. return $redirect_url;
  1099. else
  1100. return $fallback_url;
  1101. }
  1102. //
  1103. // Generate a random password of length $len
  1104. // Compatibility wrapper for random_key
  1105. //
  1106. function random_pass($len)
  1107. {
  1108. return random_key($len, true);
  1109. }
  1110. //
  1111. // Compute a hash of $str
  1112. //
  1113. function panther_hash($str)
  1114. {
  1115. return hash('sha512', $str);
  1116. }
  1117. //
  1118. // Try to determine the correct remote IP-address
  1119. //
  1120. function get_remote_address()
  1121. {
  1122. $remote_addr = $_SERVER['REMOTE_ADDR'];
  1123. // If we are behind a reverse proxy try to find the real users IP
  1124. if (defined('FORUM_BEHIND_REVERSE_PROXY'))
  1125. {
  1126. if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
  1127. {
  1128. // The general format of the field is:
  1129. // X-Forwarded-For: client1, proxy1, proxy2
  1130. // where the value is a comma+space separated list of IP addresses, the left-most being the farthest downstream client,
  1131. // and each successive proxy that passed the request adding the IP address where it received the request from.
  1132. $forwarded_for = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
  1133. $forwarded_for = trim($forwarded_for[0]);
  1134. if (@preg_match('%^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$%', $forwarded_for) || @preg_match('%^((([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){6}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(([0-9A-Fa-f]{1,4}:){0,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b))|([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})|(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})|(([0-9A-Fa-f]{1,4}:){1,7}:))$%', $forwarded_for))
  1135. $remote_addr = $forwarded_for;
  1136. }
  1137. }
  1138. return $remote_addr;
  1139. }
  1140. //
  1141. // Calls htmlspecialchars with a few options already set
  1142. // As of 1.1.0, this has been deprecated and will be removed soon. Use Twig instead.
  1143. //
  1144. function panther_htmlspecialchars($str)
  1145. {
  1146. return htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
  1147. }
  1148. //
  1149. // Calls htmlspecialchars_decode with a few options already set
  1150. //
  1151. function panther_htmlspecialchars_decode($str)
  1152. {
  1153. if (function_exists('htmlspecialchars_decode'))
  1154. return htmlspecialchars_decode($str, ENT_QUOTES);
  1155. static $translations;
  1156. if (!isset($translations))
  1157. {
  1158. $translations = get_html_translation_table(HTML_SPECIALCHARS, ENT_QUOTES);
  1159. $translations['&#039;'] = '\''; // get_html_translation_table doesn't include &#039; which is what htmlspecialchars translates ' to, but apparently that is okay?! http://bugs.php.net/bug.php?id=25927
  1160. $translations = array_flip($translations);
  1161. }
  1162. return strtr($str, $translations);
  1163. }
  1164. //
  1165. // A wrapper for utf8_strlen for compatibility
  1166. //
  1167. function panther_strlen($str)
  1168. {
  1169. return utf8_strlen($str);
  1170. }
  1171. //
  1172. // Convert \r\n and \r to \n
  1173. //
  1174. function panther_linebreaks($str)
  1175. {
  1176. return str_replace(array("\r\n", "\r"), "\n", $str);
  1177. }
  1178. //
  1179. // A wrapper for utf8_trim for compatibility
  1180. //
  1181. function panther_trim($str, $charlist = false)
  1182. {
  1183. return is_string($str) ? utf8_trim($str, $charlist) : '';
  1184. }
  1185. //
  1186. // Checks if a string is in all uppercase
  1187. //
  1188. function is_all_uppercase($string)
  1189. {
  1190. return utf8_strtoupper($string) == $string && utf8_strtolower($string) != $string;
  1191. }
  1192. //
  1193. // Inserts $element into $input at $offset
  1194. // $offset can be either a numerical offset to insert at (eg: 0 inserts at the beginning of the array)
  1195. // or a string, which is the key that the new element should be inserted before
  1196. // $key is optional: it's used when inserting a new key/value pair into an associative array
  1197. //
  1198. function array_insert(&$input, $offset, $element, $key = null)
  1199. {
  1200. if (is_null($key))
  1201. $key = $offset;
  1202. // Determine the proper offset if we're using a string
  1203. if (!is_int($offset))
  1204. $offset = array_search($offset, array_keys($input), true);
  1205. // Out of bounds checks
  1206. if ($offset > count($input))
  1207. $offset = count($input);
  1208. else if ($offset < 0)
  1209. $offset = 0;
  1210. $input = array_merge(array_slice($input, 0, $offset), array($key => $element), array_slice($input, $offset));
  1211. }
  1212. //
  1213. // Return a template object from Twig
  1214. //
  1215. function load_template($tpl_file)
  1216. {
  1217. global $panther_user, $panther_config, $tpl_manager;
  1218. $style_root = (($panther_config['o_style_path'] != 'style') ? $panther_config['o_style_path'] : PANTHER_ROOT.$panther_config['o_style_path']).'/'.$panther_user['style'].'/templates/';
  1219. if (file_exists($style_root.$tpl_file))
  1220. $tpl_file = $tpl_manager->loadTemplate('@style/'.$tpl_file);
  1221. else
  1222. $tpl_file = $tpl_manager->loadTemplate('@core/'.$tpl_file);
  1223. return $tpl_file;
  1224. }
  1225. //
  1226. // Display a message when board is in maintenance mode
  1227. //
  1228. function maintenance_message()
  1229. {
  1230. global $db, $panther_config, $lang_common, $panther_user;
  1231. // Send no-cache headers
  1232. header('Expires: Thu, 21 Jul 1977 07:30:00 GMT'); // When yours truly first set eyes on this world! :)
  1233. header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
  1234. header('Cache-Control: post-check=0, pre-check=0', false);
  1235. header('Pragma: no-cache'); // For HTTP/1.0 compatibility
  1236. // Send the Content-type header in case the web server is setup to send something else
  1237. header('Content-type: text/html; charset=utf-8');
  1238. // Deal with newlines, tabs and multiple spaces
  1239. $pattern = array("\t", ' ', ' ');
  1240. $replace = array('&#160; &#160; ', '&#160; ', ' &#160;');
  1241. $message = str_replace($pattern, $replace, $panther_config['o_maintenance_message']);
  1242. $tpl = load_template('maintenance.tpl');
  1243. echo $tpl->render(
  1244. array(
  1245. 'lang_common' => $lang_common,
  1246. 'message' => $message,
  1247. 'page_title' => generate_page_title(array($panther_config['o_board_title'], $lang_common['Maintenance'])),
  1248. 'style' => (($panther_config['o_style_dir']) != '' ? $panther_config['o_style_dir'] : get_base_url().'/style/').$panther_user['style'],
  1249. 'panther_config' => $panther_config,
  1250. )
  1251. );
  1252. // End the transaction
  1253. $db->end_transaction();
  1254. exit;
  1255. }
  1256. //
  1257. // Display $message and redirect user to $destination_url
  1258. //
  1259. function redirect($destination_url, $message)
  1260. {
  1261. global $db, $panther_config, $lang_common, $panther_user;
  1262. // Prefix with base_url (unless there's already a valid URI)
  1263. if (strpos($destination_url, 'http://') !== 0 && strpos($destination_url, 'https://') !== 0 && strpos($destination_url, '/') !== 0)
  1264. $destination_url = get_base_url(true).'/'.$destination_url;
  1265. // Do a little spring cleaning
  1266. $destination_url = preg_replace('%([\r\n])|(\%0[ad])|(;\s*data\s*:)%i', '', $destination_url);
  1267. // If the delay is 0 seconds, we might as well skip the redirect all together
  1268. if ($panther_config['o_redirect_delay'] == '0')
  1269. {
  1270. $db->end_transaction();
  1271. header('Location: '.str_replace('&amp;', '&', $destination_url));
  1272. exit;
  1273. }
  1274. // Send no-cache headers
  1275. header('Expires: Thu, 21 Jul 1977 07:30:00 GMT'); // When yours truly first set eyes on this world! :)
  1276. header('Last-Modified: '.gmdate('D, d M Y H:i:s').' GMT');
  1277. header('Cache-Control: post-check=0, pre-check=0', false);
  1278. header('Pragma: no-cache'); // For HTTP/1.0 compatibility
  1279. // Send the Content-type header in case the web server is setup to send something else
  1280. header('Content-type: text/html; charset=utf-8');
  1281. $tpl = load_template('redirect.tpl');
  1282. echo $tpl->render(
  1283. array(
  1284. 'lang_common' => $lang_common,
  1285. 'destination_url' => $destination_url,
  1286. 'message' => $message,
  1287. 'queries' => ($panther_config['o_show_queries'] == '1') ? display_saved_queries() : '',
  1288. 'panther_config' => $panther_config,
  1289. 'page_title' => generate_page_title(array($panther_config['o_board_title'], $lang_common['Redirecting'])),
  1290. 'css_url' => (($panther_config['o_style_dir']) != '' ? $panther_config['o_style_dir'] : get_base_url().'/style/').$panther_user['style'],
  1291. )
  1292. );
  1293. $db->end_transaction();
  1294. exit;
  1295. }
  1296. //
  1297. // Handle PHP errors
  1298. //
  1299. function error_handler($errno = 0, $errstr = 'Error', $errfile = 'unknown', $errline = 0)
  1300. {
  1301. global $panther_config, $lang_common, $panther_user, $panther_url;
  1302. if (!is_int($errno)) // Make sure set_exception_handler doesn't intefere
  1303. {
  1304. $errstr = $errno;
  1305. $errno = 1;
  1306. }
  1307. // Needed to ensure it doesn't appear after completion on every page
  1308. if ($errno < 1)
  1309. exit;
  1310. $error = error_get_last();
  1311. //