The version of vichan running on lainchan.org
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

2851 lignes
83KB

  1. <?php
  2. /*
  3. * Copyright (c) 2010-2014 Tinyboard Development Group
  4. */
  5. if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) {
  6. // You cannot request this file directly.
  7. exit;
  8. }
  9. define('TINYBOARD', null);
  10. $microtime_start = microtime(true);
  11. require_once 'inc/error.php';
  12. require_once 'inc/cache.php';
  13. require_once 'inc/display.php';
  14. require_once 'inc/template.php';
  15. require_once 'inc/database.php';
  16. require_once 'inc/events.php';
  17. require_once 'inc/api.php';
  18. require_once 'inc/mod/auth.php';
  19. require_once 'inc/lock.php';
  20. require_once 'inc/queue.php';
  21. require_once 'inc/polyfill.php';
  22. @include_once 'inc/lib/parsedown/Parsedown.php'; // fail silently, this isn't a critical piece of code
  23. if (!extension_loaded('gettext')) {
  24. require_once 'inc/lib/gettext/gettext.inc';
  25. }
  26. // the user is not currently logged in as a moderator
  27. $mod = false;
  28. mb_internal_encoding('UTF-8');
  29. loadConfig();
  30. function init_locale($locale) {
  31. if (extension_loaded('gettext')) {
  32. if (setlocale(LC_ALL, $locale) === false) {
  33. //$error('The specified locale (' . $locale . ') does not exist on your platform!');
  34. // Fall back to C.UTF-8 instead of normal C, so we support unicode instead of just ASCII
  35. setlocale(LC_ALL, "C.UTF-8");
  36. setlocale(LC_CTYPE, "C.UTF-8");
  37. }
  38. bindtextdomain('tinyboard', './inc/locale');
  39. bind_textdomain_codeset('tinyboard', 'UTF-8');
  40. textdomain('tinyboard');
  41. } else {
  42. if (_setlocale(LC_ALL, $locale) === false) {
  43. error('The specified locale (' . $locale . ') does not exist on your platform!');
  44. // Fall back to C.UTF-8 instead of normal C, so we support unicode instead of just ASCII
  45. _setlocale(LC_ALL, "C.UTF-8");
  46. _setlocale(LC_CTYPE, "C.UTF-8");
  47. }
  48. _bindtextdomain('tinyboard', './inc/locale');
  49. _bind_textdomain_codeset('tinyboard', 'UTF-8');
  50. _textdomain('tinyboard');
  51. }
  52. }
  53. $current_locale = 'en';
  54. function loadConfig() {
  55. global $board, $config, $__ip, $debug, $__version, $microtime_start, $current_locale, $events;
  56. $boardsuffix = isset($board['uri']) ? $board['uri'] : '';
  57. if (!isset($_SERVER['REMOTE_ADDR']))
  58. $_SERVER['REMOTE_ADDR'] = '0.0.0.0';
  59. if (file_exists('tmp/cache/cache_config.php')) {
  60. require_once('tmp/cache/cache_config.php');
  61. }
  62. if (isset($config['cache_config']) &&
  63. $config['cache_config'] &&
  64. $config = Cache::get('config_' . $boardsuffix ) ) {
  65. $events = Cache::get('events_' . $boardsuffix );
  66. define_groups();
  67. if (file_exists('inc/instance-functions.php')) {
  68. require_once('inc/instance-functions.php');
  69. }
  70. if ($config['locale'] != $current_locale) {
  71. $current_locale = $config['locale'];
  72. init_locale($config['locale']);
  73. }
  74. }
  75. else {
  76. $config = array();
  77. reset_events();
  78. $arrays = array(
  79. 'db',
  80. 'api',
  81. 'cache',
  82. 'lock',
  83. 'queue',
  84. 'cookies',
  85. 'error',
  86. 'dir',
  87. 'mod',
  88. 'spam',
  89. 'filters',
  90. 'wordfilters',
  91. 'custom_capcode',
  92. 'custom_tripcode',
  93. 'dnsbl',
  94. 'dnsbl_exceptions',
  95. 'remote',
  96. 'allowed_ext',
  97. 'allowed_ext_files',
  98. 'file_icons',
  99. 'footer',
  100. 'stylesheets',
  101. 'code_stylesheets',
  102. 'additional_javascript',
  103. 'markup',
  104. 'custom_pages',
  105. 'dashboard_links'
  106. );
  107. foreach ($arrays as $key) {
  108. $config[$key] = array();
  109. }
  110. if (!file_exists('inc/instance-config.php'))
  111. error('Tinyboard is not configured! Create inc/instance-config.php.');
  112. // Initialize locale as early as possible
  113. // Those calls are expensive. Unfortunately, our cache system is not initialized at this point.
  114. // So, we may store the locale in a tmp/ filesystem.
  115. if (file_exists($fn = 'tmp/cache/locale_' . $boardsuffix ) ) {
  116. $config['locale'] = @file_get_contents($fn);
  117. }
  118. else {
  119. $config['locale'] = 'en';
  120. $configstr = file_get_contents('inc/instance-config.php');
  121. if (isset($board['dir']) && file_exists($board['dir'] . '/config.php')) {
  122. $configstr .= file_get_contents($board['dir'] . '/config.php');
  123. }
  124. $matches = array();
  125. preg_match_all('/[^\/#*]\$config\s*\[\s*[\'"]locale[\'"]\s*\]\s*=\s*([\'"])(.*?)\1/', $configstr, $matches);
  126. if ($matches && isset ($matches[2]) && $matches[2]) {
  127. $matches = $matches[2];
  128. $config['locale'] = $matches[count($matches)-1];
  129. }
  130. @file_put_contents($fn, $config['locale']);
  131. }
  132. if ($config['locale'] != $current_locale) {
  133. $current_locale = $config['locale'];
  134. init_locale($config['locale']);
  135. }
  136. require 'inc/config.php';
  137. require 'inc/instance-config.php';
  138. if (isset($board['dir']) && file_exists($board['dir'] . '/config.php')) {
  139. require $board['dir'] . '/config.php';
  140. }
  141. if ($config['locale'] != $current_locale) {
  142. $current_locale = $config['locale'];
  143. init_locale($config['locale']);
  144. }
  145. if (!isset($config['global_message']))
  146. $config['global_message'] = false;
  147. if (!isset($config['post_url']))
  148. $config['post_url'] = $config['root'] . $config['file_post'];
  149. if (!isset($config['referer_match']))
  150. if (isset($_SERVER['HTTP_HOST'])) {
  151. $config['referer_match'] = '/^' .
  152. (preg_match('@^https?://@', $config['root']) ? '' :
  153. 'https?:\/\/' . $_SERVER['HTTP_HOST']) .
  154. preg_quote($config['root'], '/') .
  155. '(' .
  156. str_replace('%s', $config['board_regex'], preg_quote($config['board_path'], '/')) .
  157. '(' .
  158. preg_quote($config['file_index'], '/') . '|' .
  159. str_replace('%d', '\d+', preg_quote($config['file_page'])) .
  160. ')?' .
  161. '|' .
  162. str_replace('%s', $config['board_regex'], preg_quote($config['board_path'], '/')) .
  163. preg_quote($config['dir']['res'], '/') .
  164. '(' .
  165. str_replace('%d', '\d+', preg_quote($config['file_page'], '/')) . '|' .
  166. str_replace('%d', '\d+', preg_quote($config['file_page50'], '/')) . '|' .
  167. str_replace(array('%d', '%s'), array('\d+', '[a-z0-9-]+'), preg_quote($config['file_page_slug'], '/')) . '|' .
  168. str_replace(array('%d', '%s'), array('\d+', '[a-z0-9-]+'), preg_quote($config['file_page50_slug'], '/')) .
  169. ')' .
  170. '|' .
  171. preg_quote($config['file_mod'], '/') . '\?\/.+' .
  172. ')([#?](.+)?)?$/ui';
  173. } else {
  174. // CLI mode
  175. $config['referer_match'] = '//';
  176. }
  177. if (!isset($config['cookies']['path']))
  178. $config['cookies']['path'] = &$config['root'];
  179. if (!isset($config['dir']['static']))
  180. $config['dir']['static'] = $config['root'] . 'static/';
  181. if (!isset($config['image_blank']))
  182. $config['image_blank'] = $config['dir']['static'] . 'blank.gif';
  183. if (!isset($config['image_sticky']))
  184. $config['image_sticky'] = $config['dir']['static'] . 'sticky.gif';
  185. if (!isset($config['image_locked']))
  186. $config['image_locked'] = $config['dir']['static'] . 'locked.gif';
  187. if (!isset($config['image_bumplocked']))
  188. $config['image_bumplocked'] = $config['dir']['static'] . 'sage.gif';
  189. if (!isset($config['image_deleted']))
  190. $config['image_deleted'] = $config['dir']['static'] . 'deleted.png';
  191. if (!isset($config['uri_thumb']))
  192. $config['uri_thumb'] = $config['root'] . $board['dir'] . $config['dir']['thumb'];
  193. elseif (isset($board['dir']))
  194. $config['uri_thumb'] = sprintf($config['uri_thumb'], $board['dir']);
  195. if (!isset($config['uri_img']))
  196. $config['uri_img'] = $config['root'] . $board['dir'] . $config['dir']['img'];
  197. elseif (isset($board['dir']))
  198. $config['uri_img'] = sprintf($config['uri_img'], $board['dir']);
  199. if (!isset($config['uri_stylesheets']))
  200. $config['uri_stylesheets'] = $config['root'] . 'stylesheets/';
  201. if (!isset($config['url_stylesheet']))
  202. $config['url_stylesheet'] = $config['uri_stylesheets'] . 'style.css';
  203. if (!isset($config['url_javascript']))
  204. $config['url_javascript'] = $config['root'] . $config['file_script'];
  205. if (!isset($config['additional_javascript_url']))
  206. $config['additional_javascript_url'] = $config['root'];
  207. if (!isset($config['uri_flags']))
  208. $config['uri_flags'] = $config['root'] . 'static/flags/%s.png';
  209. if (!isset($config['user_flag']))
  210. $config['user_flag'] = false;
  211. if (!isset($config['user_flags']))
  212. $config['user_flags'] = array();
  213. if (!isset($__version))
  214. $__version = file_exists('.installed') ? trim(file_get_contents('.installed')) : false;
  215. $config['version'] = $__version;
  216. if ($config['allow_roll'])
  217. event_handler('post', 'diceRoller');
  218. if (in_array('webm', $config['allowed_ext_files']) ||
  219. in_array('mp4', $config['allowed_ext_files']))
  220. event_handler('post', 'postHandler');
  221. }
  222. // Effectful config processing below:
  223. date_default_timezone_set($config['timezone']);
  224. if ($config['root_file']) {
  225. chdir($config['root_file']);
  226. }
  227. // Keep the original address to properly comply with other board configurations
  228. if (!isset($__ip))
  229. $__ip = $_SERVER['REMOTE_ADDR'];
  230. // ::ffff:0.0.0.0
  231. if (preg_match('/^\:\:(ffff\:)?(\d+\.\d+\.\d+\.\d+)$/', $__ip, $m))
  232. $_SERVER['REMOTE_ADDR'] = $m[2];
  233. if ($config['verbose_errors']) {
  234. set_error_handler('error_handler');
  235. error_reporting(E_ALL);
  236. ini_set('display_errors', true);
  237. ini_set('html_errors', false);
  238. } else {
  239. ini_set('display_errors', false);
  240. }
  241. if ($config['syslog'])
  242. openlog('tinyboard', LOG_ODELAY, LOG_SYSLOG); // open a connection to sysem logger
  243. if ($config['recaptcha'])
  244. require_once 'inc/lib/recaptcha/recaptchalib.php';
  245. if ($config['cache']['enabled'])
  246. require_once 'inc/cache.php';
  247. if (in_array('webm', $config['allowed_ext_files']) ||
  248. in_array('mp4', $config['allowed_ext_files']))
  249. require_once 'inc/lib/webm/posthandler.php';
  250. event('load-config');
  251. if ($config['cache_config'] && !isset ($config['cache_config_loaded'])) {
  252. file_put_contents('tmp/cache/cache_config.php', '<?php '.
  253. '$config = array();'.
  254. '$config[\'cache\'] = '.var_export($config['cache'], true).';'.
  255. '$config[\'cache_config\'] = true;'.
  256. '$config[\'debug\'] = '.var_export($config['debug'], true).';'.
  257. 'require_once(\'inc/cache.php\');'
  258. );
  259. $config['cache_config_loaded'] = true;
  260. Cache::set('config_'.$boardsuffix, $config);
  261. Cache::set('events_'.$boardsuffix, $events);
  262. }
  263. if (is_array($config['anonymous'])) {
  264. $config['anonymous'] = $config['anonymous'][array_rand($config['anonymous'])];
  265. }
  266. else if (is_callable($config['anonymous'])){
  267. $config['anonymous'] = $config['anonymous']($boardsuffix);
  268. }
  269. if ($config['debug']) {
  270. if (!isset($debug)) {
  271. $debug = array(
  272. 'sql' => array(),
  273. 'exec' => array(),
  274. 'purge' => array(),
  275. 'cached' => array(),
  276. 'write' => array(),
  277. 'time' => array(
  278. 'db_queries' => 0,
  279. 'exec' => 0,
  280. ),
  281. 'start' => $microtime_start,
  282. 'start_debug' => microtime(true)
  283. );
  284. $debug['start'] = $microtime_start;
  285. }
  286. }
  287. }
  288. function _syslog($priority, $message) {
  289. if (isset($_SERVER['REMOTE_ADDR'], $_SERVER['REQUEST_METHOD'], $_SERVER['REQUEST_URI'])) {
  290. // CGI
  291. syslog($priority, $message . ' - client: ' . $_SERVER['REMOTE_ADDR'] . ', request: "' . $_SERVER['REQUEST_METHOD'] . ' ' . $_SERVER['REQUEST_URI'] . '"');
  292. } else {
  293. syslog($priority, $message);
  294. }
  295. }
  296. function define_groups() {
  297. global $config;
  298. foreach ($config['mod']['groups'] as $group_value => $group_name) {
  299. $group_name = strtoupper($group_name);
  300. if(!defined($group_name)) {
  301. define($group_name, $group_value, true);
  302. }
  303. }
  304. ksort($config['mod']['groups']);
  305. }
  306. function create_antibot($board, $thread = null) {
  307. require_once dirname(__FILE__) . '/anti-bot.php';
  308. return _create_antibot($board, $thread);
  309. }
  310. function rebuildThemes($action, $boardname = false) {
  311. global $config, $board, $current_locale;
  312. // Save the global variables
  313. $_config = $config;
  314. $_board = $board;
  315. // List themes
  316. if ($themes = Cache::get("themes")) {
  317. // OK, we already have themes loaded
  318. }
  319. else {
  320. $query = query("SELECT `theme` FROM ``theme_settings`` WHERE `name` IS NULL AND `value` IS NULL") or error(db_error());
  321. $themes = array();
  322. while ($theme = $query->fetch(PDO::FETCH_ASSOC)) {
  323. $themes[] = $theme;
  324. }
  325. Cache::set("themes", $themes);
  326. }
  327. foreach ($themes as $theme) {
  328. // Restore them
  329. $config = $_config;
  330. $board = $_board;
  331. // Reload the locale
  332. if ($config['locale'] != $current_locale) {
  333. $current_locale = $config['locale'];
  334. init_locale($config['locale']);
  335. }
  336. if (PHP_SAPI === 'cli') {
  337. echo "Rebuilding theme ".$theme['theme']."... ";
  338. }
  339. rebuildTheme($theme['theme'], $action, $boardname);
  340. if (PHP_SAPI === 'cli') {
  341. echo "done\n";
  342. }
  343. }
  344. // Restore them again
  345. $config = $_config;
  346. $board = $_board;
  347. // Reload the locale
  348. if ($config['locale'] != $current_locale) {
  349. $current_locale = $config['locale'];
  350. init_locale($config['locale']);
  351. }
  352. }
  353. function loadThemeConfig($_theme) {
  354. global $config;
  355. if (!file_exists($config['dir']['themes'] . '/' . $_theme . '/info.php'))
  356. return false;
  357. // Load theme information into $theme
  358. include $config['dir']['themes'] . '/' . $_theme . '/info.php';
  359. return $theme;
  360. }
  361. function rebuildTheme($theme, $action, $board = false) {
  362. global $config, $_theme;
  363. $_theme = $theme;
  364. $theme = loadThemeConfig($_theme);
  365. if (file_exists($config['dir']['themes'] . '/' . $_theme . '/theme.php')) {
  366. require_once $config['dir']['themes'] . '/' . $_theme . '/theme.php';
  367. $theme['build_function']($action, themeSettings($_theme), $board);
  368. }
  369. }
  370. function themeSettings($theme) {
  371. if ($settings = Cache::get("theme_settings_".$theme)) {
  372. return $settings;
  373. }
  374. $query = prepare("SELECT `name`, `value` FROM ``theme_settings`` WHERE `theme` = :theme AND `name` IS NOT NULL");
  375. $query->bindValue(':theme', $theme);
  376. $query->execute() or error(db_error($query));
  377. $settings = array();
  378. while ($s = $query->fetch(PDO::FETCH_ASSOC)) {
  379. $settings[$s['name']] = $s['value'];
  380. }
  381. Cache::set("theme_settings_".$theme, $settings);
  382. return $settings;
  383. }
  384. function sprintf3($str, $vars, $delim = '%') {
  385. $replaces = array();
  386. foreach ($vars as $k => $v) {
  387. $replaces[$delim . $k . $delim] = $v;
  388. }
  389. return str_replace(array_keys($replaces),
  390. array_values($replaces), $str);
  391. }
  392. function mb_substr_replace($string, $replacement, $start, $length) {
  393. return mb_substr($string, 0, $start) . $replacement . mb_substr($string, $start + $length);
  394. }
  395. function setupBoard($array) {
  396. global $board, $config;
  397. $board = array(
  398. 'uri' => $array['uri'],
  399. 'title' => $array['title'],
  400. 'subtitle' => $array['subtitle'],
  401. #'indexed' => $array['indexed'],
  402. );
  403. // older versions
  404. $board['name'] = &$board['title'];
  405. $board['dir'] = sprintf($config['board_path'], $board['uri']);
  406. $board['url'] = sprintf($config['board_abbreviation'], $board['uri']);
  407. loadConfig();
  408. if (!file_exists($board['dir']))
  409. @mkdir($board['dir'], 0777) or error("Couldn't create " . $board['dir'] . ". Check permissions.", true);
  410. if (!file_exists($board['dir'] . $config['dir']['img']))
  411. @mkdir($board['dir'] . $config['dir']['img'], 0777)
  412. or error("Couldn't create " . $board['dir'] . $config['dir']['img'] . ". Check permissions.", true);
  413. if (!file_exists($board['dir'] . $config['dir']['thumb']))
  414. @mkdir($board['dir'] . $config['dir']['thumb'], 0777)
  415. or error("Couldn't create " . $board['dir'] . $config['dir']['img'] . ". Check permissions.", true);
  416. if (!file_exists($board['dir'] . $config['dir']['res']))
  417. @mkdir($board['dir'] . $config['dir']['res'], 0777)
  418. or error("Couldn't create " . $board['dir'] . $config['dir']['img'] . ". Check permissions.", true);
  419. }
  420. function openBoard($uri) {
  421. global $config, $build_pages, $board;
  422. if ($config['try_smarter'])
  423. $build_pages = array();
  424. // And what if we don't really need to change a board we have opened?
  425. if (isset ($board) && isset ($board['uri']) && $board['uri'] == $uri) {
  426. return true;
  427. }
  428. $b = getBoardInfo($uri);
  429. if ($b) {
  430. setupBoard($b);
  431. if (function_exists('after_open_board')) {
  432. after_open_board();
  433. }
  434. return true;
  435. }
  436. return false;
  437. }
  438. function getBoardInfo($uri) {
  439. global $config;
  440. if ($config['cache']['enabled'] && ($board = cache::get('board_' . $uri))) {
  441. return $board;
  442. }
  443. $query = prepare("SELECT * FROM ``boards`` WHERE `uri` = :uri LIMIT 1");
  444. $query->bindValue(':uri', $uri);
  445. $query->execute() or error(db_error($query));
  446. if ($board = $query->fetch(PDO::FETCH_ASSOC)) {
  447. if ($config['cache']['enabled'])
  448. cache::set('board_' . $uri, $board);
  449. return $board;
  450. }
  451. return false;
  452. }
  453. function boardTitle($uri) {
  454. $board = getBoardInfo($uri);
  455. if ($board)
  456. return $board['title'];
  457. return false;
  458. }
  459. function purge($uri) {
  460. global $config, $debug;
  461. // Fix for Unicode
  462. $uri = rawurlencode($uri);
  463. $noescape = "/!~*()+:";
  464. $noescape = preg_split('//', $noescape);
  465. $noescape_url = array_map("rawurlencode", $noescape);
  466. $uri = str_replace($noescape_url, $noescape, $uri);
  467. if (preg_match($config['referer_match'], $config['root']) && isset($_SERVER['REQUEST_URI'])) {
  468. $uri = (str_replace('\\', '/', dirname($_SERVER['REQUEST_URI'])) == '/' ? '/' : str_replace('\\', '/', dirname($_SERVER['REQUEST_URI'])) . '/') . $uri;
  469. } else {
  470. $uri = $config['root'] . $uri;
  471. }
  472. if ($config['debug']) {
  473. $debug['purge'][] = $uri;
  474. }
  475. foreach ($config['purge'] as &$purge) {
  476. $host = &$purge[0];
  477. $port = &$purge[1];
  478. $http_host = isset($purge[2]) ? $purge[2] : (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost');
  479. $request = "PURGE {$uri} HTTP/1.1\r\nHost: {$http_host}\r\nUser-Agent: Tinyboard\r\nConnection: Close\r\n\r\n";
  480. if ($fp = fsockopen($host, $port, $errno, $errstr, $config['purge_timeout'])) {
  481. fwrite($fp, $request);
  482. fclose($fp);
  483. } else {
  484. // Cannot connect?
  485. error('Could not PURGE for ' . $host);
  486. }
  487. }
  488. }
  489. function file_write($path, $data, $simple = false, $skip_purge = false) {
  490. global $config, $debug;
  491. if (preg_match('/^remote:\/\/(.+)\:(.+)$/', $path, $m)) {
  492. if (isset($config['remote'][$m[1]])) {
  493. require_once 'inc/remote.php';
  494. $remote = new Remote($config['remote'][$m[1]]);
  495. $remote->write($data, $m[2]);
  496. return;
  497. } else {
  498. error('Invalid remote server: ' . $m[1]);
  499. }
  500. }
  501. if (!$fp = fopen($path, $simple ? 'w' : 'c'))
  502. error('Unable to open file for writing: ' . $path);
  503. // File locking
  504. if (!$simple && !flock($fp, LOCK_EX)) {
  505. error('Unable to lock file: ' . $path);
  506. }
  507. // Truncate file
  508. if (!$simple && !ftruncate($fp, 0))
  509. error('Unable to truncate file: ' . $path);
  510. // Write data
  511. if (($bytes = fwrite($fp, $data)) === false)
  512. error('Unable to write to file: ' . $path);
  513. // Unlock
  514. if (!$simple)
  515. flock($fp, LOCK_UN);
  516. // Close
  517. if (!fclose($fp))
  518. error('Unable to close file: ' . $path);
  519. /**
  520. * Create gzipped file.
  521. *
  522. * When writing into a file foo.bar and the size is larger or equal to 1
  523. * KiB, this also produces the gzipped version foo.bar.gz
  524. *
  525. * This is useful with nginx with gzip_static on.
  526. */
  527. if ($config['gzip_static']) {
  528. $gzpath = "$path.gz";
  529. if ($bytes & ~0x3ff) { // if ($bytes >= 1024)
  530. if (file_put_contents($gzpath, gzencode($data), $simple ? 0 : LOCK_EX) === false)
  531. error("Unable to write to file: $gzpath");
  532. //if (!touch($gzpath, filemtime($path), fileatime($path)))
  533. // error("Unable to touch file: $gzpath");
  534. }
  535. else {
  536. @unlink($gzpath);
  537. }
  538. }
  539. if (!$skip_purge && isset($config['purge'])) {
  540. // Purge cache
  541. if (basename($path) == $config['file_index']) {
  542. // Index file (/index.html); purge "/" as well
  543. $uri = dirname($path);
  544. // root
  545. if ($uri == '.')
  546. $uri = '';
  547. else
  548. $uri .= '/';
  549. purge($uri);
  550. }
  551. purge($path);
  552. }
  553. if ($config['debug']) {
  554. $debug['write'][] = $path . ': ' . $bytes . ' bytes';
  555. }
  556. event('write', $path);
  557. }
  558. function file_unlink($path) {
  559. global $config, $debug;
  560. if ($config['debug']) {
  561. if (!isset($debug['unlink']))
  562. $debug['unlink'] = array();
  563. $debug['unlink'][] = $path;
  564. }
  565. $ret = @unlink($path);
  566. if ($config['gzip_static']) {
  567. $gzpath = "$path.gz";
  568. @unlink($gzpath);
  569. }
  570. if (isset($config['purge']) && $path[0] != '/' && isset($_SERVER['HTTP_HOST'])) {
  571. // Purge cache
  572. if (basename($path) == $config['file_index']) {
  573. // Index file (/index.html); purge "/" as well
  574. $uri = dirname($path);
  575. // root
  576. if ($uri == '.')
  577. $uri = '';
  578. else
  579. $uri .= '/';
  580. purge($uri);
  581. }
  582. purge($path);
  583. }
  584. event('unlink', $path);
  585. return $ret;
  586. }
  587. function hasPermission($action = null, $board = null, $_mod = null) {
  588. global $config;
  589. if (isset($_mod))
  590. $mod = &$_mod;
  591. else
  592. global $mod;
  593. if (!is_array($mod))
  594. return false;
  595. if (isset($action) && $mod['type'] < $action)
  596. return false;
  597. if (!isset($board) || $config['mod']['skip_per_board'])
  598. return true;
  599. if (!isset($mod['boards']))
  600. return false;
  601. if (!in_array('*', $mod['boards']) && !in_array($board, $mod['boards']))
  602. return false;
  603. return true;
  604. }
  605. function listBoards($just_uri = false) {
  606. global $config;
  607. $just_uri ? $cache_name = 'all_boards_uri' : $cache_name = 'all_boards';
  608. if ($config['cache']['enabled'] && ($boards = cache::get($cache_name)))
  609. return $boards;
  610. if (!$just_uri) {
  611. $query = query("SELECT * FROM ``boards`` ORDER BY `uri`") or error(db_error());
  612. $boards = $query->fetchAll();
  613. } else {
  614. $boards = array();
  615. $query = query("SELECT `uri` FROM ``boards``") or error(db_error());
  616. while ($board = $query->fetchColumn()) {
  617. $boards[] = $board;
  618. }
  619. }
  620. if ($config['cache']['enabled'])
  621. cache::set($cache_name, $boards);
  622. return $boards;
  623. }
  624. function until($timestamp) {
  625. $difference = $timestamp - time();
  626. switch(TRUE){
  627. case ($difference < 60):
  628. return $difference . ' ' . ngettext('second', 'seconds', $difference);
  629. case ($difference < 3600): //60*60 = 3600
  630. return ($num = round($difference/(60))) . ' ' . ngettext('minute', 'minutes', $num);
  631. case ($difference < 86400): //60*60*24 = 86400
  632. return ($num = round($difference/(3600))) . ' ' . ngettext('hour', 'hours', $num);
  633. case ($difference < 604800): //60*60*24*7 = 604800
  634. return ($num = round($difference/(86400))) . ' ' . ngettext('day', 'days', $num);
  635. case ($difference < 31536000): //60*60*24*365 = 31536000
  636. return ($num = round($difference/(604800))) . ' ' . ngettext('week', 'weeks', $num);
  637. default:
  638. return ($num = round($difference/(31536000))) . ' ' . ngettext('year', 'years', $num);
  639. }
  640. }
  641. function ago($timestamp) {
  642. $difference = time() - $timestamp;
  643. switch(TRUE){
  644. case ($difference < 60) :
  645. return $difference . ' ' . ngettext('second', 'seconds', $difference);
  646. case ($difference < 3600): //60*60 = 3600
  647. return ($num = round($difference/(60))) . ' ' . ngettext('minute', 'minutes', $num);
  648. case ($difference < 86400): //60*60*24 = 86400
  649. return ($num = round($difference/(3600))) . ' ' . ngettext('hour', 'hours', $num);
  650. case ($difference < 604800): //60*60*24*7 = 604800
  651. return ($num = round($difference/(86400))) . ' ' . ngettext('day', 'days', $num);
  652. case ($difference < 31536000): //60*60*24*365 = 31536000
  653. return ($num = round($difference/(604800))) . ' ' . ngettext('week', 'weeks', $num);
  654. default:
  655. return ($num = round($difference/(31536000))) . ' ' . ngettext('year', 'years', $num);
  656. }
  657. }
  658. function displayBan($ban) {
  659. global $config, $board;
  660. if (!$ban['seen']) {
  661. Bans::seen($ban['id']);
  662. }
  663. $ban['ip'] = $_SERVER['REMOTE_ADDR'];
  664. if ($ban['post'] && isset($ban['post']['board'], $ban['post']['id'])) {
  665. if (openBoard($ban['post']['board'])) {
  666. $query = query(sprintf("SELECT `files` FROM ``posts_%s`` WHERE `id` = " .
  667. (int)$ban['post']['id'], $board['uri']));
  668. if ($_post = $query->fetch(PDO::FETCH_ASSOC)) {
  669. $ban['post'] = array_merge($ban['post'], $_post);
  670. }
  671. }
  672. if ($ban['post']['thread']) {
  673. $post = new Post($ban['post']);
  674. } else {
  675. $post = new Thread($ban['post'], null, false, false);
  676. }
  677. }
  678. $denied_appeals = array();
  679. $pending_appeal = false;
  680. if ($config['ban_appeals']) {
  681. $query = query("SELECT `time`, `denied` FROM ``ban_appeals`` WHERE `ban_id` = " . (int)$ban['id']) or error(db_error());
  682. while ($ban_appeal = $query->fetch(PDO::FETCH_ASSOC)) {
  683. if ($ban_appeal['denied']) {
  684. $denied_appeals[] = $ban_appeal['time'];
  685. } else {
  686. $pending_appeal = $ban_appeal['time'];
  687. }
  688. }
  689. }
  690. // Show banned page and exit
  691. die(
  692. Element('page.html', array(
  693. 'title' => _('Banned!'),
  694. 'config' => $config,
  695. 'boardlist' => createBoardlist(isset($mod) ? $mod : false),
  696. 'body' => Element('banned.html', array(
  697. 'config' => $config,
  698. 'ban' => $ban,
  699. 'board' => $board,
  700. 'post' => isset($post) ? $post->build(true) : false,
  701. 'denied_appeals' => $denied_appeals,
  702. 'pending_appeal' => $pending_appeal
  703. )
  704. ))
  705. ));
  706. }
  707. function checkBan($board = false) {
  708. global $config;
  709. if (!isset($_SERVER['REMOTE_ADDR'])) {
  710. // Server misconfiguration
  711. return;
  712. }
  713. if (event('check-ban', $board))
  714. return true;
  715. $ips = array();
  716. $ips[] = $_SERVER['REMOTE_ADDR'];
  717. if ($config['proxy_check'] && isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
  718. $ips = array_merge($ips, explode(", ", $_SERVER['HTTP_X_FORWARDED_FOR']));
  719. }
  720. foreach ($ips as $ip) {
  721. $bans = Bans::find($_SERVER['REMOTE_ADDR'], $board, $config['show_modname']);
  722. foreach ($bans as &$ban) {
  723. if ($ban['expires'] && $ban['expires'] < time()) {
  724. Bans::delete($ban['id']);
  725. if ($config['require_ban_view'] && !$ban['seen']) {
  726. if (!isset($_POST['json_response'])) {
  727. displayBan($ban);
  728. } else {
  729. header('Content-Type: text/json');
  730. die(json_encode(array('error' => true, 'banned' => true)));
  731. }
  732. }
  733. } else {
  734. if (!isset($_POST['json_response'])) {
  735. displayBan($ban);
  736. } else {
  737. header('Content-Type: text/json');
  738. die(json_encode(array('error' => true, 'banned' => true)));
  739. }
  740. }
  741. }
  742. }
  743. // I'm not sure where else to put this. It doesn't really matter where; it just needs to be called every
  744. // now and then to keep the ban list tidy.
  745. if ($config['cache']['enabled'] && $last_time_purged = cache::get('purged_bans_last')) {
  746. if (time() - $last_time_purged < $config['purge_bans'] )
  747. return;
  748. }
  749. Bans::purge();
  750. if ($config['cache']['enabled'])
  751. cache::set('purged_bans_last', time());
  752. }
  753. function threadLocked($id) {
  754. global $board;
  755. if (event('check-locked', $id))
  756. return true;
  757. $query = prepare(sprintf("SELECT `locked` FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL LIMIT 1", $board['uri']));
  758. $query->bindValue(':id', $id, PDO::PARAM_INT);
  759. $query->execute() or error(db_error());
  760. if (($locked = $query->fetchColumn()) === false) {
  761. // Non-existant, so it can't be locked...
  762. return false;
  763. }
  764. return (bool)$locked;
  765. }
  766. function threadSageLocked($id) {
  767. global $board;
  768. if (event('check-sage-locked', $id))
  769. return true;
  770. $query = prepare(sprintf("SELECT `sage` FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL LIMIT 1", $board['uri']));
  771. $query->bindValue(':id', $id, PDO::PARAM_INT);
  772. $query->execute() or error(db_error());
  773. if (($sagelocked = $query->fetchColumn()) === false) {
  774. // Non-existant, so it can't be locked...
  775. return false;
  776. }
  777. return (bool)$sagelocked;
  778. }
  779. function threadExists($id) {
  780. global $board;
  781. $query = prepare(sprintf("SELECT 1 FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL LIMIT 1", $board['uri']));
  782. $query->bindValue(':id', $id, PDO::PARAM_INT);
  783. $query->execute() or error(db_error());
  784. if ($query->rowCount()) {
  785. return true;
  786. }
  787. return false;
  788. }
  789. function insertFloodPost(array $post) {
  790. global $board;
  791. $query = prepare("INSERT INTO ``flood`` VALUES (NULL, :ip, :board, :time, :posthash, :filehash, :isreply)");
  792. $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
  793. $query->bindValue(':board', $board['uri']);
  794. $query->bindValue(':time', time());
  795. $query->bindValue(':posthash', make_comment_hex($post['body_nomarkup']));
  796. if ($post['has_file'])
  797. $query->bindValue(':filehash', $post['filehash']);
  798. else
  799. $query->bindValue(':filehash', null, PDO::PARAM_NULL);
  800. $query->bindValue(':isreply', !$post['op'], PDO::PARAM_INT);
  801. $query->execute() or error(db_error($query));
  802. }
  803. function post(array $post) {
  804. global $pdo, $board,$config;
  805. $query = prepare(sprintf("INSERT INTO ``posts_%s`` VALUES ( NULL, :thread, :subject, :email, :name, :trip, :capcode, :body, :body_nomarkup, :time, :time, :files, :num_files, :filehash, :password, :ip, :sticky, :locked, :cycle, 0, :embed, :slug)", $board['uri']));
  806. // Basic stuff
  807. if (!empty($post['subject'])) {
  808. $query->bindValue(':subject', $post['subject']);
  809. } else {
  810. $query->bindValue(':subject', null, PDO::PARAM_NULL);
  811. }
  812. if (!empty($post['email'])) {
  813. $query->bindValue(':email', $post['email']);
  814. } else {
  815. $query->bindValue(':email', null, PDO::PARAM_NULL);
  816. }
  817. if (!empty($post['trip'])) {
  818. $query->bindValue(':trip', $post['trip']);
  819. } else {
  820. $query->bindValue(':trip', null, PDO::PARAM_NULL);
  821. }
  822. $query->bindValue(':name', $post['name']);
  823. $query->bindValue(':body', $post['body']);
  824. $query->bindValue(':body_nomarkup', $post['body_nomarkup']);
  825. $query->bindValue(':time', isset($post['time']) ? $post['time'] : time(), PDO::PARAM_INT);
  826. $query->bindValue(':password', $post['password']);
  827. $query->bindValue(':ip', isset($post['ip']) ? $post['ip'] : $_SERVER['REMOTE_ADDR']);
  828. if ($post['op'] && $post['mod'] && isset($post['sticky']) && $post['sticky']) {
  829. $query->bindValue(':sticky', true, PDO::PARAM_INT);
  830. } else {
  831. $query->bindValue(':sticky', false, PDO::PARAM_INT);
  832. }
  833. if ($post['op'] && $post['mod'] && isset($post['locked']) && $post['locked']) {
  834. $query->bindValue(':locked', true, PDO::PARAM_INT);
  835. } else {
  836. $query->bindValue(':locked', false, PDO::PARAM_INT);
  837. }
  838. if ($post['op'] && $post['mod'] && isset($post['cycle']) && $post['cycle']) {
  839. $query->bindValue(':cycle', true, PDO::PARAM_INT);
  840. } else {
  841. $query->bindValue(':cycle', false, PDO::PARAM_INT);
  842. }
  843. if ($post['mod'] && isset($post['capcode']) && $post['capcode']) {
  844. $query->bindValue(':capcode', $post['capcode'], PDO::PARAM_INT);
  845. } else {
  846. if ($config['joke_capcode'] && isset($post['capcode']) && $post['capcode'] === 'joke') {
  847. $query->bindValue(':capcode', $post['capcode'], PDO::PARAM_INT);
  848. }
  849. else {
  850. $query->bindValue(':capcode', null, PDO::PARAM_NULL);
  851. }
  852. }
  853. if (!empty($post['embed'])) {
  854. $query->bindValue(':embed', $post['embed']);
  855. } else {
  856. $query->bindValue(':embed', null, PDO::PARAM_NULL);
  857. }
  858. if ($post['op']) {
  859. // No parent thread, image
  860. $query->bindValue(':thread', null, PDO::PARAM_NULL);
  861. } else {
  862. $query->bindValue(':thread', $post['thread'], PDO::PARAM_INT);
  863. }
  864. if ($post['has_file']) {
  865. $query->bindValue(':files', json_encode($post['files']));
  866. $query->bindValue(':num_files', $post['num_files']);
  867. $query->bindValue(':filehash', $post['filehash']);
  868. } else {
  869. $query->bindValue(':files', null, PDO::PARAM_NULL);
  870. $query->bindValue(':num_files', 0);
  871. $query->bindValue(':filehash', null, PDO::PARAM_NULL);
  872. }
  873. if ($post['op']) {
  874. $query->bindValue(':slug', slugify($post));
  875. }
  876. else {
  877. $query->bindValue(':slug', NULL);
  878. }
  879. if (!$query->execute()) {
  880. undoImage($post);
  881. error(db_error($query));
  882. }
  883. return $pdo->lastInsertId();
  884. }
  885. function bumpThread($id) {
  886. global $config, $board, $build_pages;
  887. if (event('bump', $id))
  888. return true;
  889. if ($config['try_smarter']) {
  890. $build_pages = array_merge(range(1, thread_find_page($id)), $build_pages);
  891. }
  892. $query = prepare(sprintf("UPDATE ``posts_%s`` SET `bump` = :time WHERE `id` = :id AND `thread` IS NULL", $board['uri']));
  893. $query->bindValue(':time', time(), PDO::PARAM_INT);
  894. $query->bindValue(':id', $id, PDO::PARAM_INT);
  895. $query->execute() or error(db_error($query));
  896. }
  897. // Remove file from post
  898. function deleteFile($id, $remove_entirely_if_already=true, $file=null) {
  899. global $board, $config;
  900. $query = prepare(sprintf("SELECT `thread`, `files`, `num_files` FROM ``posts_%s`` WHERE `id` = :id LIMIT 1", $board['uri']));
  901. $query->bindValue(':id', $id, PDO::PARAM_INT);
  902. $query->execute() or error(db_error($query));
  903. if (!$post = $query->fetch(PDO::FETCH_ASSOC))
  904. error($config['error']['invalidpost']);
  905. $files = json_decode($post['files']);
  906. $file_to_delete = $file !== false ? $files[(int)$file] : (object)array('file' => false);
  907. if (!$files[0]) error(_('That post has no files.'));
  908. if ($files[0]->file == 'deleted' && $post['num_files'] == 1 && !$post['thread'])
  909. return; // Can't delete OP's image completely.
  910. $query = prepare(sprintf("UPDATE ``posts_%s`` SET `files` = :file WHERE `id` = :id", $board['uri']));
  911. if (($file && $file_to_delete->file == 'deleted') && $remove_entirely_if_already) {
  912. // Already deleted; remove file fully
  913. $files[$file] = null;
  914. } else {
  915. foreach ($files as $i => $f) {
  916. if (($file !== false && $i == $file) || $file === null) {
  917. // Delete thumbnail
  918. if (isset ($f->thumb) && $f->thumb) {
  919. file_unlink($board['dir'] . $config['dir']['thumb'] . $f->thumb);
  920. unset($files[$i]->thumb);
  921. }
  922. // Delete file
  923. file_unlink($board['dir'] . $config['dir']['img'] . $f->file);
  924. $files[$i]->file = 'deleted';
  925. }
  926. }
  927. }
  928. $query->bindValue(':file', json_encode($files), PDO::PARAM_STR);
  929. $query->bindValue(':id', $id, PDO::PARAM_INT);
  930. $query->execute() or error(db_error($query));
  931. if ($post['thread'])
  932. buildThread($post['thread']);
  933. else
  934. buildThread($id);
  935. }
  936. // rebuild post (markup)
  937. function rebuildPost($id) {
  938. global $board, $mod;
  939. $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE `id` = :id", $board['uri']));
  940. $query->bindValue(':id', $id, PDO::PARAM_INT);
  941. $query->execute() or error(db_error($query));
  942. if ((!$post = $query->fetch(PDO::FETCH_ASSOC)) || !$post['body_nomarkup'])
  943. return false;
  944. markup($post['body'] = &$post['body_nomarkup']);
  945. $post = (object)$post;
  946. event('rebuildpost', $post);
  947. $post = (array)$post;
  948. $query = prepare(sprintf("UPDATE ``posts_%s`` SET `body` = :body WHERE `id` = :id", $board['uri']));
  949. $query->bindValue(':body', $post['body']);
  950. $query->bindValue(':id', $id, PDO::PARAM_INT);
  951. $query->execute() or error(db_error($query));
  952. buildThread($post['thread'] ? $post['thread'] : $id);
  953. return true;
  954. }
  955. // Delete a post (reply or thread)
  956. function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) {
  957. global $board, $config;
  958. // Select post and replies (if thread) in one query
  959. $query = prepare(sprintf("SELECT `id`,`thread`,`files`,`slug` FROM ``posts_%s`` WHERE `id` = :id OR `thread` = :id", $board['uri']));
  960. $query->bindValue(':id', $id, PDO::PARAM_INT);
  961. $query->execute() or error(db_error($query));
  962. if ($query->rowCount() < 1) {
  963. if ($error_if_doesnt_exist)
  964. error($config['error']['invalidpost']);
  965. else return false;
  966. }
  967. $ids = array();
  968. // Delete posts and maybe replies
  969. while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
  970. event('delete', $post);
  971. if (!$post['thread']) {
  972. // Delete thread HTML page
  973. file_unlink($board['dir'] . $config['dir']['res'] . link_for($post) );
  974. file_unlink($board['dir'] . $config['dir']['res'] . link_for($post, true) ); // noko50
  975. file_unlink($board['dir'] . $config['dir']['res'] . sprintf('%d.json', $post['id']));
  976. $antispam_query = prepare('DELETE FROM ``antispam`` WHERE `board` = :board AND `thread` = :thread');
  977. $antispam_query->bindValue(':board', $board['uri']);
  978. $antispam_query->bindValue(':thread', $post['id']);
  979. $antispam_query->execute() or error(db_error($antispam_query));
  980. } elseif ($query->rowCount() == 1) {
  981. // Rebuild thread
  982. $rebuild = &$post['thread'];
  983. }
  984. if ($post['files']) {
  985. // Delete file
  986. foreach (json_decode($post['files']) as $i => $f) {
  987. if ($f->file !== 'deleted') {
  988. file_unlink($board['dir'] . $config['dir']['img'] . $f->file);
  989. file_unlink($board['dir'] . $config['dir']['thumb'] . $f->thumb);
  990. }
  991. }
  992. }
  993. $ids[] = (int)$post['id'];
  994. }
  995. $query = prepare(sprintf("DELETE FROM ``posts_%s`` WHERE `id` = :id OR `thread` = :id", $board['uri']));
  996. $query->bindValue(':id', $id, PDO::PARAM_INT);
  997. $query->execute() or error(db_error($query));
  998. $query = prepare("SELECT `board`, `post` FROM ``cites`` WHERE `target_board` = :board AND (`target` = " . implode(' OR `target` = ', $ids) . ") ORDER BY `board`");
  999. $query->bindValue(':board', $board['uri']);
  1000. $query->execute() or error(db_error($query));
  1001. while ($cite = $query->fetch(PDO::FETCH_ASSOC)) {
  1002. if ($board['uri'] != $cite['board']) {
  1003. if (!isset($tmp_board))
  1004. $tmp_board = $board['uri'];
  1005. openBoard($cite['board']);
  1006. }
  1007. rebuildPost($cite['post']);
  1008. }
  1009. if (isset($tmp_board))
  1010. openBoard($tmp_board);
  1011. $query = prepare("DELETE FROM ``cites`` WHERE (`target_board` = :board AND (`target` = " . implode(' OR `target` = ', $ids) . ")) OR (`board` = :board AND (`post` = " . implode(' OR `post` = ', $ids) . "))");
  1012. $query->bindValue(':board', $board['uri']);
  1013. $query->execute() or error(db_error($query));
  1014. if (isset($rebuild) && $rebuild_after) {
  1015. buildThread($rebuild);
  1016. buildIndex();
  1017. }
  1018. return true;
  1019. }
  1020. function clean($pid = false) {
  1021. global $board, $config;
  1022. $offset = round($config['max_pages']*$config['threads_per_page']);
  1023. // I too wish there was an easier way of doing this...
  1024. $query = prepare(sprintf("SELECT `id` FROM ``posts_%s`` WHERE `thread` IS NULL ORDER BY `sticky` DESC, `bump` DESC LIMIT :offset, 9001", $board['uri']));
  1025. $query->bindValue(':offset', $offset, PDO::PARAM_INT);
  1026. $query->execute() or error(db_error($query));
  1027. while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
  1028. deletePost($post['id'], false, false);
  1029. if ($pid) modLog("Automatically deleting thread #{$post['id']} due to new thread #{$pid}");
  1030. }
  1031. // Bump off threads with X replies earlier, spam prevention method
  1032. if ($config['early_404']) {
  1033. $offset = round($config['early_404_page']*$config['threads_per_page']);
  1034. $query = prepare(sprintf("SELECT `id` AS `thread_id`, (SELECT COUNT(`id`) FROM ``posts_%s`` WHERE `thread` = `thread_id`) AS `reply_count` FROM ``posts_%s`` WHERE `thread` IS NULL ORDER BY `sticky` DESC, `bump` DESC LIMIT :offset, 9001", $board['uri'], $board['uri']));
  1035. $query->bindValue(':offset', $offset, PDO::PARAM_INT);
  1036. $query->execute() or error(db_error($query));
  1037. while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
  1038. if ($post['reply_count'] < $config['early_404_replies']) {
  1039. deletePost($post['thread_id'], false, false);
  1040. if ($pid) modLog("Automatically deleting thread #{$post['thread_id']} due to new thread #{$pid} (early 404 is set, #{$post['thread_id']} had {$post['reply_count']} replies)");
  1041. }
  1042. }
  1043. }
  1044. }
  1045. function thread_find_page($thread) {
  1046. global $config, $board;
  1047. $query = query(sprintf("SELECT `id` FROM ``posts_%s`` WHERE `thread` IS NULL ORDER BY `sticky` DESC, `bump` DESC", $board['uri'])) or error(db_error($query));
  1048. $threads = $query->fetchAll(PDO::FETCH_COLUMN);
  1049. if (($index = array_search($thread, $threads)) === false)
  1050. return false;
  1051. return floor(($config['threads_per_page'] + $index) / $config['threads_per_page']);
  1052. }
  1053. // $brief means that we won't need to generate anything yet
  1054. function index($page, $mod=false, $brief = false) {
  1055. global $board, $config, $debug;
  1056. $body = '';
  1057. $offset = round($page*$config['threads_per_page']-$config['threads_per_page']);
  1058. $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE `thread` IS NULL ORDER BY `sticky` DESC, `bump` DESC LIMIT :offset,:threads_per_page", $board['uri']));
  1059. $query->bindValue(':offset', $offset, PDO::PARAM_INT);
  1060. $query->bindValue(':threads_per_page', $config['threads_per_page'], PDO::PARAM_INT);
  1061. $query->execute() or error(db_error($query));
  1062. if ($page == 1 && $query->rowCount() < $config['threads_per_page'])
  1063. $board['thread_count'] = $query->rowCount();
  1064. if ($query->rowCount() < 1 && $page > 1)
  1065. return false;
  1066. $threads = array();
  1067. while ($th = $query->fetch(PDO::FETCH_ASSOC)) {
  1068. $thread = new Thread($th, $mod ? '?/' : $config['root'], $mod);
  1069. if ($config['cache']['enabled']) {
  1070. $cached = cache::get("thread_index_{$board['uri']}_{$th['id']}");
  1071. if (isset($cached['replies'], $cached['omitted'])) {
  1072. $replies = $cached['replies'];
  1073. $omitted = $cached['omitted'];
  1074. } else {
  1075. unset($cached);
  1076. }
  1077. }
  1078. if (!isset($cached)) {
  1079. $posts = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE `thread` = :id ORDER BY `id` DESC LIMIT :limit", $board['uri']));
  1080. $posts->bindValue(':id', $th['id']);
  1081. $posts->bindValue(':limit', ($th['sticky'] ? $config['threads_preview_sticky'] : $config['threads_preview']), PDO::PARAM_INT);
  1082. $posts->execute() or error(db_error($posts));
  1083. $replies = array_reverse($posts->fetchAll(PDO::FETCH_ASSOC));
  1084. if (count($replies) == ($th['sticky'] ? $config['threads_preview_sticky'] : $config['threads_preview'])) {
  1085. $count = numPosts($th['id']);
  1086. $omitted = array('post_count' => $count['replies'], 'image_count' => $count['images']);
  1087. } else {
  1088. $omitted = false;
  1089. }
  1090. if ($config['cache']['enabled'])
  1091. cache::set("thread_index_{$board['uri']}_{$th['id']}", array(
  1092. 'replies' => $replies,
  1093. 'omitted' => $omitted,
  1094. ));
  1095. }
  1096. $num_images = 0;
  1097. foreach ($replies as $po) {
  1098. if ($po['num_files'])
  1099. $num_images+=$po['num_files'];
  1100. $thread->add(new Post($po, $mod ? '?/' : $config['root'], $mod));
  1101. }
  1102. $thread->images = $num_images;
  1103. $thread->replies = isset($omitted['post_count']) ? $omitted['post_count'] : count($replies);
  1104. if ($omitted) {
  1105. $thread->omitted = $omitted['post_count'] - ($th['sticky'] ? $config['threads_preview_sticky'] : $config['threads_preview']);
  1106. $thread->omitted_images = $omitted['image_count'] - $num_images;
  1107. }
  1108. $threads[] = $thread;
  1109. if (!$brief) {
  1110. $body .= $thread->build(true);
  1111. }
  1112. }
  1113. if ($config['file_board']) {
  1114. $body = Element('fileboard.html', array('body' => $body, 'mod' => $mod));
  1115. }
  1116. return array(
  1117. 'board' => $board,
  1118. 'body' => $body,
  1119. 'post_url' => $config['post_url'],
  1120. 'config' => $config,
  1121. 'boardlist' => createBoardlist($mod),
  1122. 'threads' => $threads,
  1123. );
  1124. }
  1125. function getPageButtons($pages, $mod=false) {
  1126. global $config, $board;
  1127. $btn = array();
  1128. $root = ($mod ? '?/' : $config['root']) . $board['dir'];
  1129. foreach ($pages as $num => $page) {
  1130. if (isset($page['selected'])) {
  1131. // Previous button
  1132. if ($num == 0) {
  1133. // There is no previous page.
  1134. $btn['prev'] = _('Previous');
  1135. } else {
  1136. $loc = ($mod ? '?/' . $board['uri'] . '/' : '') .
  1137. ($num == 1 ?
  1138. $config['file_index']
  1139. :
  1140. sprintf($config['file_page'], $num)
  1141. );
  1142. $btn['prev'] = '<form action="' . ($mod ? '' : $root . $loc) . '" method="get">' .
  1143. ($mod ?
  1144. '<input type="hidden" name="status" value="301" />' .
  1145. '<input type="hidden" name="r" value="' . htmlentities($loc) . '" />'
  1146. :'') .
  1147. '<input type="submit" value="' . _('Previous') . '" /></form>';
  1148. }
  1149. if ($num == count($pages) - 1) {
  1150. // There is no next page.
  1151. $btn['next'] = _('Next');
  1152. } else {
  1153. $loc = ($mod ? '?/' . $board['uri'] . '/' : '') . sprintf($config['file_page'], $num + 2);
  1154. $btn['next'] = '<form action="' . ($mod ? '' : $root . $loc) . '" method="get">' .
  1155. ($mod ?
  1156. '<input type="hidden" name="status" value="301" />' .
  1157. '<input type="hidden" name="r" value="' . htmlentities($loc) . '" />'
  1158. :'') .
  1159. '<input type="submit" value="' . _('Next') . '" /></form>';
  1160. }
  1161. }
  1162. }
  1163. return $btn;
  1164. }
  1165. function getPages($mod=false) {
  1166. global $board, $config;
  1167. if (isset($board['thread_count'])) {
  1168. $count = $board['thread_count'];
  1169. } else {
  1170. // Count threads
  1171. $query = query(sprintf("SELECT COUNT(*) FROM ``posts_%s`` WHERE `thread` IS NULL", $board['uri'])) or error(db_error());
  1172. $count = $query->fetchColumn();
  1173. }
  1174. $count = floor(($config['threads_per_page'] + $count - 1) / $config['threads_per_page']);
  1175. if ($count < 1) $count = 1;
  1176. $pages = array();
  1177. for ($x=0;$x<$count && $x<$config['max_pages'];$x++) {
  1178. $pages[] = array(
  1179. 'num' => $x+1,
  1180. 'link' => $x==0 ? ($mod ? '?/' : $config['root']) . $board['dir'] . $config['file_index'] : ($mod ? '?/' : $config['root']) . $board['dir'] . sprintf($config['file_page'], $x+1)
  1181. );
  1182. }
  1183. return $pages;
  1184. }
  1185. // Stolen with permission from PlainIB (by Frank Usrs)
  1186. function make_comment_hex($str) {
  1187. // remove cross-board citations
  1188. // the numbers don't matter
  1189. $str = preg_replace('!>>>/[A-Za-z0-9]+/!', '', $str);
  1190. if (function_exists('iconv')) {
  1191. // remove diacritics and other noise
  1192. // FIXME: this removes cyrillic entirely
  1193. $oldstr = $str;
  1194. $str = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str);
  1195. if (!$str) $str = $oldstr;
  1196. }
  1197. $str = strtolower($str);
  1198. // strip all non-alphabet characters
  1199. $str = preg_replace('/[^a-z]/', '', $str);
  1200. return md5($str);
  1201. }
  1202. function makerobot($body) {
  1203. global $config;
  1204. $body = strtolower($body);
  1205. // Leave only letters
  1206. $body = preg_replace('/[^a-z]/i', '', $body);
  1207. // Remove repeating characters
  1208. if ($config['robot_strip_repeating'])
  1209. $body = preg_replace('/(.)\\1+/', '$1', $body);
  1210. return sha1($body);
  1211. }
  1212. function checkRobot($body) {
  1213. if (empty($body) || event('check-robot', $body))
  1214. return true;
  1215. $body = makerobot($body);
  1216. $query = prepare("SELECT 1 FROM ``robot`` WHERE `hash` = :hash LIMIT 1");
  1217. $query->bindValue(':hash', $body);
  1218. $query->execute() or error(db_error($query));
  1219. if ($query->fetchColumn()) {
  1220. return true;
  1221. }
  1222. // Insert new hash
  1223. $query = prepare("INSERT INTO ``robot`` VALUES (:hash)");
  1224. $query->bindValue(':hash', $body);
  1225. $query->execute() or error(db_error($query));
  1226. return false;
  1227. }
  1228. // Returns an associative array with 'replies' and 'images' keys
  1229. function numPosts($id) {
  1230. global $board;
  1231. $query = prepare(sprintf("SELECT COUNT(*) AS `replies`, SUM(`num_files`) AS `images` FROM ``posts_%s`` WHERE `thread` = :thread", $board['uri'], $board['uri']));
  1232. $query->bindValue(':thread', $id, PDO::PARAM_INT);
  1233. $query->execute() or error(db_error($query));
  1234. return $query->fetch(PDO::FETCH_ASSOC);
  1235. }
  1236. function muteTime() {
  1237. global $config;
  1238. if ($time = event('mute-time'))
  1239. return $time;
  1240. // Find number of mutes in the past X hours
  1241. $query = prepare("SELECT COUNT(*) FROM ``mutes`` WHERE `time` >= :time AND `ip` = :ip");
  1242. $query->bindValue(':time', time()-($config['robot_mute_hour']*3600), PDO::PARAM_INT);
  1243. $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
  1244. $query->execute() or error(db_error($query));
  1245. if (!$result = $query->fetchColumn())
  1246. return 0;
  1247. return pow($config['robot_mute_multiplier'], $result);
  1248. }
  1249. function mute() {
  1250. // Insert mute
  1251. $query = prepare("INSERT INTO ``mutes`` VALUES (:ip, :time)");
  1252. $query->bindValue(':time', time(), PDO::PARAM_INT);
  1253. $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
  1254. $query->execute() or error(db_error($query));
  1255. return muteTime();
  1256. }
  1257. function checkMute() {
  1258. global $config, $debug;
  1259. if ($config['cache']['enabled']) {
  1260. // Cached mute?
  1261. if (($mute = cache::get("mute_${_SERVER['REMOTE_ADDR']}")) && ($mutetime = cache::get("mutetime_${_SERVER['REMOTE_ADDR']}"))) {
  1262. error(sprintf($config['error']['youaremuted'], $mute['time'] + $mutetime - time()));
  1263. }
  1264. }
  1265. $mutetime = muteTime();
  1266. if ($mutetime > 0) {
  1267. // Find last mute time
  1268. $query = prepare("SELECT `time` FROM ``mutes`` WHERE `ip` = :ip ORDER BY `time` DESC LIMIT 1");
  1269. $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']);
  1270. $query->execute() or error(db_error($query));
  1271. if (!$mute = $query->fetch(PDO::FETCH_ASSOC)) {
  1272. // What!? He's muted but he's not muted...
  1273. return;
  1274. }
  1275. if ($mute['time'] + $mutetime > time()) {
  1276. if ($config['cache']['enabled']) {
  1277. cache::set("mute_${_SERVER['REMOTE_ADDR']}", $mute, $mute['time'] + $mutetime - time());
  1278. cache::set("mutetime_${_SERVER['REMOTE_ADDR']}", $mutetime, $mute['time'] + $mutetime - time());
  1279. }
  1280. // Not expired yet
  1281. error(sprintf($config['error']['youaremuted'], $mute['time'] + $mutetime - time()));
  1282. } else {
  1283. // Already expired
  1284. return;
  1285. }
  1286. }
  1287. }
  1288. function buildIndex($global_api = "yes") {
  1289. global $board, $config, $build_pages;
  1290. $catalog_api_action = generation_strategy('sb_api', array($board['uri']));
  1291. $pages = null;
  1292. $antibot = null;
  1293. if ($config['api']['enabled']) {
  1294. $api = new Api();
  1295. $catalog = array();
  1296. }
  1297. for ($page = 1; $page <= $config['max_pages']; $page++) {
  1298. $filename = $board['dir'] . ($page == 1 ? $config['file_index'] : sprintf($config['file_page'], $page));
  1299. $jsonFilename = $board['dir'] . ($page - 1) . '.json'; // pages should start from 0
  1300. $wont_build_this_page = $config['try_smarter'] && isset($build_pages) && !empty($build_pages) && !in_array($page, $build_pages);
  1301. if ((!$config['api']['enabled'] || $global_api == "skip") && $wont_build_this_page)
  1302. continue;
  1303. $action = generation_strategy('sb_board', array($board['uri'], $page));
  1304. if ($action == 'rebuild' || $catalog_api_action == 'rebuild') {
  1305. $content = index($page, false, $wont_build_this_page);
  1306. if (!$content)
  1307. break;
  1308. // json api
  1309. if ($config['api']['enabled']) {
  1310. $threads = $content['threads'];
  1311. $json = json_encode($api->translatePage($threads));
  1312. file_write($jsonFilename, $json);
  1313. $catalog[$page-1] = $threads;
  1314. if ($wont_build_this_page) continue;
  1315. }
  1316. if ($config['try_smarter']) {
  1317. $antibot = create_antibot($board['uri'], 0 - $page);
  1318. $content['current_page'] = $page;
  1319. }
  1320. elseif (!$antibot) {
  1321. $antibot = create_antibot($board['uri']);
  1322. }
  1323. $antibot->reset();
  1324. if (!$pages) {
  1325. $pages = getPages();
  1326. }
  1327. $content['pages'] = $pages;
  1328. $content['pages'][$page-1]['selected'] = true;
  1329. $content['btn'] = getPageButtons($content['pages']);
  1330. $content['antibot'] = $antibot;
  1331. file_write($filename, Element('index.html', $content));
  1332. }
  1333. elseif ($action == 'delete' || $catalog_api_action == 'delete') {
  1334. file_unlink($filename);
  1335. file_unlink($jsonFilename);
  1336. }
  1337. }
  1338. // $action is an action for our last page
  1339. if (($catalog_api_action == 'rebuild' || $action == 'rebuild' || $action == 'delete') && $page < $config['max_pages']) {
  1340. for (;$page<=$config['max_pages'];$page++) {
  1341. $filename = $board['dir'] . ($page==1 ? $config['file_index'] : sprintf($config['file_page'], $page));
  1342. file_unlink($filename);
  1343. if ($config['api']['enabled']) {
  1344. $jsonFilename = $board['dir'] . ($page - 1) . '.json';
  1345. file_unlink($jsonFilename);
  1346. }
  1347. }
  1348. }
  1349. // json api catalog
  1350. if ($config['api']['enabled'] && $global_api != "skip") {
  1351. if ($catalog_api_action == 'delete') {
  1352. $jsonFilename = $board['dir'] . 'catalog.json';
  1353. file_unlink($jsonFilename);
  1354. $jsonFilename = $board['dir'] . 'threads.json';
  1355. file_unlink($jsonFilename);
  1356. }
  1357. elseif ($catalog_api_action == 'rebuild') {
  1358. $json = json_encode($api->translateCatalog($catalog));
  1359. $jsonFilename = $board['dir'] . 'catalog.json';
  1360. file_write($jsonFilename, $json);
  1361. $json = json_encode($api->translateCatalog($catalog, true));
  1362. $jsonFilename = $board['dir'] . 'threads.json';
  1363. file_write($jsonFilename, $json);
  1364. }
  1365. }
  1366. if ($config['try_smarter'])
  1367. $build_pages = array();
  1368. }
  1369. function buildJavascript() {
  1370. global $config;
  1371. $stylesheets = array();
  1372. foreach ($config['stylesheets'] as $name => $uri) {
  1373. $stylesheets[] = array(
  1374. 'name' => addslashes($name),
  1375. 'uri' => addslashes((!empty($uri) ? $config['uri_stylesheets'] : '') . $uri));
  1376. }
  1377. $code_stylesheets = array();
  1378. foreach ($config['code_stylesheets'] as $name => $uri) {
  1379. $code_stylesheets[] = array(
  1380. 'name' => addslashes($name),
  1381. 'uri' => addslashes((!empty($uri) ? $config['uri_stylesheets'] : '') . $uri));
  1382. }
  1383. $script = Element('main.js', array(
  1384. 'config' => $config,
  1385. 'stylesheets' => $stylesheets,
  1386. 'code_stylesheets' => $code_stylesheets
  1387. ));
  1388. // Check if we have translation for the javascripts; if yes, we add it to additional javascripts
  1389. list($pure_locale) = explode(".", $config['locale']);
  1390. if (file_exists ($jsloc = "inc/locale/$pure_locale/LC_MESSAGES/javascript.js")) {
  1391. $script = file_get_contents($jsloc) . "\n\n" . $script;
  1392. }
  1393. if ($config['additional_javascript_compile']) {
  1394. foreach ($config['additional_javascript'] as $file) {
  1395. $script .= file_get_contents($file);
  1396. }
  1397. }
  1398. if ($config['minify_js']) {
  1399. require_once 'inc/lib/minify/JSMin.php';
  1400. $script = JSMin::minify($script);
  1401. }
  1402. file_write($config['file_script'], $script);
  1403. }
  1404. function checkDNSBL() {
  1405. global $config;
  1406. if (isIPv6())
  1407. return; // No IPv6 support yet.
  1408. if (!isset($_SERVER['REMOTE_ADDR']))
  1409. return; // Fix your web server configuration
  1410. if (preg_match("/^(::(ffff:)?)?(127\.|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|0\.|255\.)/", $_SERVER['REMOTE_ADDR']))
  1411. return; // It's pointless to check for local IP addresses in dnsbls, isn't it?
  1412. if (in_array($_SERVER['REMOTE_ADDR'], $config['dnsbl_exceptions']))
  1413. return;
  1414. $ipaddr = ReverseIPOctets($_SERVER['REMOTE_ADDR']);
  1415. foreach ($config['dnsbl'] as $blacklist) {
  1416. if (!is_array($blacklist))
  1417. $blacklist = array($blacklist);
  1418. if (($lookup = str_replace('%', $ipaddr, $blacklist[0])) == $blacklist[0])
  1419. $lookup = $ipaddr . '.' . $blacklist[0];
  1420. if (!$ip = DNS($lookup))
  1421. continue; // not in list
  1422. $blacklist_name = isset($blacklist[2]) ? $blacklist[2] : $blacklist[0];
  1423. if (!isset($blacklist[1])) {
  1424. // If you're listed at all, you're blocked.
  1425. error(sprintf($config['error']['dnsbl'], $blacklist_name));
  1426. } elseif (is_array($blacklist[1])) {
  1427. foreach ($blacklist[1] as $octet) {
  1428. if ($ip == $octet || $ip == '127.0.0.' . $octet)
  1429. error(sprintf($config['error']['dnsbl'], $blacklist_name));
  1430. }
  1431. } elseif (is_callable($blacklist[1])) {
  1432. if ($blacklist[1]($ip))
  1433. error(sprintf($config['error']['dnsbl'], $blacklist_name));
  1434. } else {
  1435. if ($ip == $blacklist[1] || $ip == '127.0.0.' . $blacklist[1])
  1436. error(sprintf($config['error']['dnsbl'], $blacklist_name));
  1437. }
  1438. }
  1439. }
  1440. function isIPv6() {
  1441. return strstr($_SERVER['REMOTE_ADDR'], ':') !== false;
  1442. }
  1443. function ReverseIPOctets($ip) {
  1444. return implode('.', array_reverse(explode('.', $ip)));
  1445. }
  1446. function wordfilters(&$body) {
  1447. global $config;
  1448. foreach ($config['wordfilters'] as $filter) {
  1449. if (isset($filter[3]) && $filter[3]) {
  1450. $refilter = $filter[0];
  1451. if (strncmp($filter[0], "/", 1) !== 0)
  1452. {
  1453. $refilter = "/.*" . $filter[0] . "/";
  1454. }
  1455. $body = preg_replace_callback($refilter,
  1456. function($matches) use ($filter , $body) {
  1457. foreach ($matches as $match) {
  1458. if (preg_match("/(http|https|ftp):\/\//i", $match)) {
  1459. return $match;
  1460. } else {
  1461. if (isset($filter[2]) && $filter[2]) {
  1462. $refilter = $filter[0];
  1463. if (strncmp($filter[0], "/", 1) !== 0)
  1464. {
  1465. $refilter = "/.*" . $filter[0] . "/";
  1466. }
  1467. if (is_callable($filter[1]))
  1468. return preg_replace_callback($refilter, $filter[1], $match);
  1469. else
  1470. return preg_replace($refilter, $filter[1], $match);
  1471. } else {
  1472. return str_ireplace($filter[0], $filter[1], $match);
  1473. }
  1474. }
  1475. }
  1476. }
  1477. , $body);
  1478. } else {
  1479. if (isset($filter[2]) && $filter[2]) {
  1480. if (is_callable($filter[1]))
  1481. $body = preg_replace_callback($filter[0], $filter[1], $body);
  1482. else
  1483. $body = preg_replace($filter[0], $filter[1], $body);
  1484. } else {
  1485. $body = str_ireplace($filter[0], $filter[1], $body);
  1486. }
  1487. }
  1488. }
  1489. }
  1490. function quote($body, $quote=true) {
  1491. global $config;
  1492. $body = str_replace('<br/>', "\n", $body);
  1493. $body = strip_tags($body);
  1494. $body = preg_replace("/(^|\n)/", '$1&gt;', $body);
  1495. $body .= "\n";
  1496. if ($config['minify_html'])
  1497. $body = str_replace("\n", '&#010;', $body);
  1498. return $body;
  1499. }
  1500. function markup_url($matches) {
  1501. global $config, $markup_urls;
  1502. $url = $matches[1];
  1503. $after = $matches[2];
  1504. $markup_urls[] = $url;
  1505. $link = (object) array(
  1506. 'href' => $config['link_prefix'] . $url,
  1507. 'text' => $url,
  1508. 'rel' => 'nofollow',
  1509. 'target' => '_blank',
  1510. );
  1511. event('markup-url', $link);
  1512. $link = (array)$link;
  1513. $parts = array();
  1514. foreach ($link as $attr => $value) {
  1515. if ($attr == 'text' || $attr == 'after')
  1516. continue;
  1517. $parts[] = $attr . '="' . $value . '"';
  1518. }
  1519. if (isset($link['after']))
  1520. $after = $link['after'] . $after;
  1521. return '<a ' . implode(' ', $parts) . '>' . $link['text'] . '</a>' . $after;
  1522. }
  1523. function unicodify($body) {
  1524. $body = str_replace('...', '&hellip;', $body);
  1525. $body = str_replace('&lt;--', '&larr;', $body);
  1526. $body = str_replace('--&gt;', '&rarr;', $body);
  1527. // En and em- dashes are rendered exactly the same in
  1528. // most monospace fonts (they look the same in code
  1529. // editors).
  1530. $body = str_replace('---', '&mdash;', $body); // em dash
  1531. $body = str_replace('--', '&ndash;', $body); // en dash
  1532. return $body;
  1533. }
  1534. function extract_modifiers($body) {
  1535. $modifiers = array();
  1536. if (preg_match_all('@<tinyboard ([\w\s]+)>(.*?)</tinyboard>@us', $body, $matches, PREG_SET_ORDER)) {
  1537. foreach ($matches as $match) {
  1538. if (preg_match('/^escape /', $match[1]))
  1539. continue;
  1540. $modifiers[$match[1]] = html_entity_decode($match[2]);
  1541. }
  1542. }
  1543. return $modifiers;
  1544. }
  1545. function remove_modifiers($body) {
  1546. return preg_replace('@<tinyboard ([\w\s]+)>(.+?)</tinyboard>@usm', '', $body);
  1547. }
  1548. function markup(&$body, $track_cites = false, $op = false) {
  1549. global $board, $config, $markup_urls;
  1550. $modifiers = extract_modifiers($body);
  1551. $body = preg_replace('@<tinyboard (?!escape )([\w\s]+)>(.+?)</tinyboard>@us', '', $body);
  1552. $body = preg_replace('@<(tinyboard) escape ([\w\s]+)>@i', '<$1 $2>', $body);
  1553. if (isset($modifiers['raw html']) && $modifiers['raw html'] == '1') {
  1554. return array();
  1555. }
  1556. $body = str_replace("\r", '', $body);
  1557. $body = utf8tohtml($body);
  1558. if (mysql_version() < 50503)
  1559. $body = mb_encode_numericentity($body, array(0x010000, 0xffffff, 0, 0xffffff), 'UTF-8');
  1560. if ($config['markup_code']) {
  1561. $code_markup = array();
  1562. $body = preg_replace_callback($config['markup_code'], function($matches) use (&$code_markup) {
  1563. $d = count($code_markup);
  1564. $code_markup[] = $matches;
  1565. return "<code $d>";
  1566. }, $body);
  1567. }
  1568. foreach ($config['markup'] as $markup) {
  1569. if (is_string($markup[1])) {
  1570. $body = preg_replace($markup[0], $markup[1], $body);
  1571. } elseif (is_callable($markup[1])) {
  1572. $body = preg_replace_callback($markup[0], $markup[1], $body);
  1573. }
  1574. }
  1575. if ($config['markup_urls']) {
  1576. $markup_urls = array();
  1577. $body = preg_replace_callback(
  1578. '/((?:https?:\/\/|ftp:\/\/|irc:\/\/)[^\s<>()"]+?(?:\([^\s<>()"]*?\)[^\s<>()"]*?)*)((?:\s|<|>|"|\.||\]|!|\?|,|&#44;|&quot;)*(?:[\s<>()"]|$))/',
  1579. 'markup_url',
  1580. $body,
  1581. -1,
  1582. $num_links);
  1583. if ($num_links > $config['max_links'])
  1584. error($config['error']['toomanylinks']);
  1585. }
  1586. if ($config['markup_repair_tidy'])
  1587. $body = str_replace(' ', ' &nbsp;', $body);
  1588. if ($config['auto_unicode']) {
  1589. $body = unicodify($body);
  1590. if ($config['markup_urls']) {
  1591. foreach ($markup_urls as &$url) {
  1592. $body = str_replace(unicodify($url), $url, $body);
  1593. }
  1594. }
  1595. }
  1596. $tracked_cites = array();
  1597. // Cites
  1598. if (isset($board) && preg_match_all('/(^|\s)&gt;&gt;(\d+?)([\s,.)?]|$)/m', $body, $cites, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
  1599. if (count($cites[0]) > $config['max_cites']) {
  1600. error($config['error']['toomanycites']);
  1601. }
  1602. $skip_chars = 0;
  1603. $body_tmp = $body;
  1604. $search_cites = array();
  1605. foreach ($cites as $matches) {
  1606. $search_cites[] = '`id` = ' . $matches[2][0];
  1607. }
  1608. $search_cites = array_unique($search_cites);
  1609. $query = query(sprintf('SELECT `thread`, `id` FROM ``posts_%s`` WHERE ' .
  1610. implode(' OR ', $search_cites), $board['uri'])) or error(db_error());
  1611. $cited_posts = array();
  1612. while ($cited = $query->fetch(PDO::FETCH_ASSOC)) {
  1613. $cited_posts[$cited['id']] = $cited['thread'] ? $cited['thread'] : false;
  1614. }
  1615. foreach ($cites as $matches) {
  1616. $cite = $matches[2][0];
  1617. // preg_match_all is not multibyte-safe
  1618. foreach ($matches as &$match) {
  1619. $match[1] = mb_strlen(substr($body_tmp, 0, $match[1]));
  1620. }
  1621. if (isset($cited_posts[$cite])) {
  1622. $replacement = '<a onclick="highlightReply(\''.$cite.'\', event);" href="' .
  1623. $config['root'] . $board['dir'] . $config['dir']['res'] .
  1624. link_for(array('id' => $cite, 'thread' => $cited_posts[$cite])) . '#' . $cite . '">' .
  1625. '&gt;&gt;' . $cite .
  1626. '</a>';
  1627. $body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[3][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0]));
  1628. $skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[3][0]) - mb_strlen($matches[0][0]);
  1629. if ($track_cites && $config['track_cites'])
  1630. $tracked_cites[] = array($board['uri'], $cite);
  1631. }
  1632. }
  1633. }
  1634. // Cross-board linking
  1635. if (preg_match_all('/(^|\s)&gt;&gt;&gt;\/(' . $config['board_regex'] . 'f?)\/(\d+)?([\s,.)?]|$)/um', $body, $cites, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
  1636. if (count($cites[0]) > $config['max_cites']) {
  1637. error($config['error']['toomanycross']);
  1638. }
  1639. $skip_chars = 0;
  1640. $body_tmp = $body;
  1641. if (isset($cited_posts)) {
  1642. // Carry found posts from local board >>X links
  1643. foreach ($cited_posts as $cite => $thread) {
  1644. $cited_posts[$cite] = $config['root'] . $board['dir'] . $config['dir']['res'] .
  1645. ($thread ? $thread : $cite) . '.html#' . $cite;
  1646. }
  1647. $cited_posts = array(
  1648. $board['uri'] => $cited_posts
  1649. );
  1650. } else
  1651. $cited_posts = array();
  1652. $crossboard_indexes = array();
  1653. $search_cites_boards = array();
  1654. foreach ($cites as $matches) {
  1655. $_board = $matches[2][0];
  1656. $cite = @$matches[3][0];
  1657. if (!isset($search_cites_boards[$_board]))
  1658. $search_cites_boards[$_board] = array();
  1659. $search_cites_boards[$_board][] = $cite;
  1660. }
  1661. $tmp_board = $board['uri'];
  1662. foreach ($search_cites_boards as $_board => $search_cites) {
  1663. $clauses = array();
  1664. foreach ($search_cites as $cite) {
  1665. if (!$cite || isset($cited_posts[$_board][$cite]))
  1666. continue;
  1667. $clauses[] = '`id` = ' . $cite;
  1668. }
  1669. $clauses = array_unique($clauses);
  1670. if ($board['uri'] != $_board) {
  1671. if (!openBoard($_board)){
  1672. if (in_array($_board,array_keys($config['boards_alias']))){
  1673. //$_board = $config['boards_alias'][$_board];
  1674. //openBoard($_board);
  1675. }
  1676. continue; // Unknown board
  1677. }
  1678. }
  1679. if (!empty($clauses)) {
  1680. $cited_posts[$_board] = array();
  1681. $query = query(sprintf('SELECT `thread`, `id`, `slug` FROM ``posts_%s`` WHERE ' .
  1682. implode(' OR ', $clauses), $board['uri'])) or error(db_error());
  1683. while ($cite = $query->fetch(PDO::FETCH_ASSOC)) {
  1684. $cited_posts[$_board][$cite['id']] = $config['root'] . $board['dir'] . $config['dir']['res'] .
  1685. link_for($cite) . '#' . $cite['id'];
  1686. }
  1687. }
  1688. $crossboard_indexes[$_board] = $config['root'] . $board['dir'] . $config['file_index'];
  1689. }
  1690. // Restore old board
  1691. if ($board['uri'] != $tmp_board)
  1692. openBoard($tmp_board);
  1693. foreach ($cites as $matches) {
  1694. $_board = $matches[2][0];
  1695. $cite = @$matches[3][0];
  1696. // preg_match_all is not multibyte-safe
  1697. foreach ($matches as &$match) {
  1698. $match[1] = mb_strlen(substr($body_tmp, 0, $match[1]));
  1699. }
  1700. if ($cite) {
  1701. if (isset($cited_posts[$_board][$cite])) {
  1702. $link = $cited_posts[$_board][$cite];
  1703. $replacement = '<a ' .
  1704. ($_board == $board['uri'] ?
  1705. 'onclick="highlightReply(\''.$cite.'\', event);" '
  1706. : '') . 'href="' . $link . '">' .
  1707. '&gt;&gt;&gt;/' . $_board . '/' . $cite .
  1708. '</a>';
  1709. $body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[4][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0]));
  1710. $skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[4][0]) - mb_strlen($matches[0][0]);
  1711. if ($track_cites && $config['track_cites'])
  1712. $tracked_cites[] = array($_board, $cite);
  1713. }
  1714. } elseif(isset($crossboard_indexes[$_board])) {
  1715. $replacement = '<a href="' . $crossboard_indexes[$_board] . '">' .
  1716. '&gt;&gt;&gt;/' . $_board . '/' .
  1717. '</a>';
  1718. $body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[4][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0]));
  1719. $skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[4][0]) - mb_strlen($matches[0][0]);
  1720. }
  1721. }
  1722. }
  1723. $tracked_cites = array_unique($tracked_cites, SORT_REGULAR);
  1724. $body = preg_replace("/^\s*&gt;.*$/m", '<span class="quote">$0</span>', $body);
  1725. if ($config['strip_superfluous_returns'])
  1726. $body = preg_replace('/\s+$/', '', $body);
  1727. $body = preg_replace("/\n/", '<br/>', $body);
  1728. // Fix code markup
  1729. if ($config['markup_code']) {
  1730. foreach ($code_markup as $id => $val) {
  1731. $code = isset($val[2]) ? $val[2] : $val[1];
  1732. $code_lang = isset($val[2]) ? $val[1] : "";
  1733. $code = rtrim(ltrim($code, "\r\n"));
  1734. $code = "<pre class='code lang-$code_lang'>".str_replace(array("\n","\t"), array("&#10;","&#9;"), htmlspecialchars($code))."</pre>";
  1735. $body = str_replace("<code $id>", $code, $body);
  1736. }
  1737. }
  1738. if ($config['markup_repair_tidy']) {
  1739. $tidy = new tidy();
  1740. $body = str_replace("\t", '&#09;', $body);
  1741. $body = $tidy->repairString($body, array(
  1742. 'doctype' => 'omit',
  1743. 'bare' => true,
  1744. 'literal-attributes' => true,
  1745. 'indent' => false,
  1746. 'show-body-only' => true,
  1747. 'wrap' => 0,
  1748. 'output-bom' => false,
  1749. 'output-html' => true,
  1750. 'newline' => 'LF',
  1751. 'quiet' => true,
  1752. ), 'utf8');
  1753. $body = str_replace("\n", '', $body);
  1754. }
  1755. // replace tabs with 8 spaces
  1756. $body = str_replace("\t", ' ', $body);
  1757. return $tracked_cites;
  1758. }
  1759. function escape_markup_modifiers($string) {
  1760. return preg_replace('@<(tinyboard) ([\w\s]+)>@mi', '<$1 escape $2>', $string);
  1761. }
  1762. function utf8tohtml($utf8) {
  1763. return htmlspecialchars($utf8, ENT_NOQUOTES, 'UTF-8');
  1764. }
  1765. function ordutf8($string, &$offset) {
  1766. $code = ord(substr($string, $offset,1));
  1767. if ($code >= 128) { // otherwise 0xxxxxxx
  1768. if ($code < 224)
  1769. $bytesnumber = 2; // 110xxxxx
  1770. else if ($code < 240)
  1771. $bytesnumber = 3; // 1110xxxx
  1772. else if ($code < 248)
  1773. $bytesnumber = 4; // 11110xxx
  1774. $codetemp = $code - 192 - ($bytesnumber > 2 ? 32 : 0) - ($bytesnumber > 3 ? 16 : 0);
  1775. for ($i = 2; $i <= $bytesnumber; $i++) {
  1776. $offset ++;
  1777. $code2 = ord(substr($string, $offset, 1)) - 128; //10xxxxxx
  1778. $codetemp = $codetemp*64 + $code2;
  1779. }
  1780. $code = $codetemp;
  1781. }
  1782. $offset += 1;
  1783. if ($offset >= strlen($string))
  1784. $offset = -1;
  1785. return $code;
  1786. }
  1787. function strip_combining_chars($str) {
  1788. $chars = preg_split('//u', $str, -1, PREG_SPLIT_NO_EMPTY);
  1789. $str = '';
  1790. foreach ($chars as $char) {
  1791. $o = 0;
  1792. $ord = ordutf8($char, $o);
  1793. if ( ($ord >= 768 && $ord <= 879) || ($ord >= 1536 && $ord <= 1791) || ($ord >= 3655 && $ord <= 3659) || ($ord >= 7616 && $ord <= 7679) || ($ord >= 8400 && $ord <= 8447) || ($ord >= 65056 && $ord <= 65071))
  1794. continue;
  1795. $str .= $char;
  1796. }
  1797. return $str;
  1798. }
  1799. function buildThread($id, $return = false, $mod = false) {
  1800. global $board, $config, $build_pages;
  1801. $id = round($id);
  1802. if (event('build-thread', $id))
  1803. return;
  1804. if ($config['cache']['enabled'] && !$mod) {
  1805. // Clear cache
  1806. cache::delete("thread_index_{$board['uri']}_{$id}");
  1807. cache::delete("thread_{$board['uri']}_{$id}");
  1808. }
  1809. if ($config['try_smarter'] && !$mod)
  1810. $build_pages[] = thread_find_page($id);
  1811. $action = generation_strategy('sb_thread', array($board['uri'], $id));
  1812. if ($action == 'rebuild' || $return || $mod) {
  1813. $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id`", $board['uri']));
  1814. $query->bindValue(':id', $id, PDO::PARAM_INT);
  1815. $query->execute() or error(db_error($query));
  1816. while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
  1817. if (!isset($thread)) {
  1818. $thread = new Thread($post, $mod ? '?/' : $config['root'], $mod);
  1819. } else {
  1820. $thread->add(new Post($post, $mod ? '?/' : $config['root'], $mod));
  1821. }
  1822. }
  1823. // Check if any posts were found
  1824. if (!isset($thread))
  1825. error($config['error']['nonexistant']);
  1826. $hasnoko50 = $thread->postCount() >= $config['noko50_min'];
  1827. $antibot = $mod || $return ? false : create_antibot($board['uri'], $id);
  1828. $body = Element('thread.html', array(
  1829. 'board' => $board,
  1830. 'thread' => $thread,
  1831. 'body' => $thread->build(),
  1832. 'config' => $config,
  1833. 'id' => $id,
  1834. 'mod' => $mod,
  1835. 'hasnoko50' => $hasnoko50,
  1836. 'isnoko50' => false,
  1837. 'antibot' => $antibot,
  1838. 'boardlist' => createBoardlist($mod),
  1839. 'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index'])
  1840. ));
  1841. // json api
  1842. if ($config['api']['enabled'] && !$mod) {
  1843. $api = new Api();
  1844. $json = json_encode($api->translateThread($thread));
  1845. $jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json';
  1846. file_write($jsonFilename, $json);
  1847. }
  1848. }
  1849. elseif($action == 'delete') {
  1850. $jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json';
  1851. file_unlink($jsonFilename);
  1852. }
  1853. if ($action == 'delete' && !$return && !$mod) {
  1854. $noko50fn = $board['dir'] . $config['dir']['res'] . link_for(array('id' => $id), true);
  1855. file_unlink($noko50fn);
  1856. file_unlink($board['dir'] . $config['dir']['res'] . link_for(array('id' => $id)));
  1857. } elseif ($return) {
  1858. return $body;
  1859. } elseif ($action == 'rebuild') {
  1860. $noko50fn = $board['dir'] . $config['dir']['res'] . link_for($thread, true);
  1861. if ($hasnoko50 || file_exists($noko50fn)) {
  1862. buildThread50($id, $return, $mod, $thread, $antibot);
  1863. }
  1864. file_write($board['dir'] . $config['dir']['res'] . link_for($thread), $body);
  1865. }
  1866. }
  1867. function buildThread50($id, $return = false, $mod = false, $thread = null, $antibot = false) {
  1868. global $board, $config, $build_pages;
  1869. $id = round($id);
  1870. if ($antibot)
  1871. $antibot->reset();
  1872. if (!$thread) {
  1873. $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id` DESC LIMIT :limit", $board['uri']));
  1874. $query->bindValue(':id', $id, PDO::PARAM_INT);
  1875. $query->bindValue(':limit', $config['noko50_count']+1, PDO::PARAM_INT);
  1876. $query->execute() or error(db_error($query));
  1877. $num_images = 0;
  1878. while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
  1879. if (!isset($thread)) {
  1880. $thread = new Thread($post, $mod ? '?/' : $config['root'], $mod);
  1881. } else {
  1882. if ($post['files'])
  1883. $num_images += $post['num_files'];
  1884. $thread->add(new Post($post, $mod ? '?/' : $config['root'], $mod));
  1885. }
  1886. }
  1887. // Check if any posts were found
  1888. if (!isset($thread))
  1889. error($config['error']['nonexistant']);
  1890. if ($query->rowCount() == $config['noko50_count']+1) {
  1891. $count = prepare(sprintf("SELECT COUNT(`id`) as `num` FROM ``posts_%s`` WHERE `thread` = :thread UNION ALL
  1892. SELECT SUM(`num_files`) FROM ``posts_%s`` WHERE `files` IS NOT NULL AND `thread` = :thread", $board['uri'], $board['uri']));
  1893. $count->bindValue(':thread', $id, PDO::PARAM_INT);
  1894. $count->execute() or error(db_error($count));
  1895. $c = $count->fetch();
  1896. $thread->omitted = $c['num'] - $config['noko50_count'];
  1897. $c = $count->fetch();
  1898. $thread->omitted_images = $c['num'] - $num_images;
  1899. }
  1900. $thread->posts = array_reverse($thread->posts);
  1901. } else {
  1902. $allPosts = $thread->posts;
  1903. $thread->posts = array_slice($allPosts, -$config['noko50_count']);
  1904. $thread->omitted += count($allPosts) - count($thread->posts);
  1905. foreach ($allPosts as $index => $post) {
  1906. if ($index == count($allPosts)-count($thread->posts))
  1907. break;
  1908. if ($post->files)
  1909. $thread->omitted_images += $post->num_files;
  1910. }
  1911. }
  1912. $hasnoko50 = $thread->postCount() >= $config['noko50_min'];
  1913. $body = Element('thread.html', array(
  1914. 'board' => $board,
  1915. 'thread' => $thread,
  1916. 'body' => $thread->build(false, true),
  1917. 'config' => $config,
  1918. 'id' => $id,
  1919. 'mod' => $mod,
  1920. 'hasnoko50' => $hasnoko50,
  1921. 'isnoko50' => true,
  1922. 'antibot' => $mod ? false : ($antibot ? $antibot : create_antibot($board['uri'], $id)),
  1923. 'boardlist' => createBoardlist($mod),
  1924. 'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index'])
  1925. ));
  1926. if ($return) {
  1927. return $body;
  1928. } else {
  1929. file_write($board['dir'] . $config['dir']['res'] . link_for($thread, true), $body);
  1930. }
  1931. }
  1932. function rrmdir($dir) {
  1933. if (is_dir($dir)) {
  1934. $objects = scandir($dir);
  1935. foreach ($objects as $object) {
  1936. if ($object != "." && $object != "..") {
  1937. if (filetype($dir."/".$object) == "dir")
  1938. rrmdir($dir."/".$object);
  1939. else
  1940. file_unlink($dir."/".$object);
  1941. }
  1942. }
  1943. reset($objects);
  1944. rmdir($dir);
  1945. }
  1946. }
  1947. function poster_id($ip, $thread) {
  1948. global $config;
  1949. if ($id = event('poster-id', $ip, $thread))
  1950. return $id;
  1951. // Confusing, hard to brute-force, but simple algorithm
  1952. return substr(sha1(sha1($ip . $config['secure_trip_salt'] . $thread) . $config['secure_trip_salt']), 0, $config['poster_id_length']);
  1953. }
  1954. function generate_tripcode($name) {
  1955. global $config;
  1956. if ($trip = event('tripcode', $name))
  1957. return $trip;
  1958. if (!preg_match('/^([^#]+)?(##|#)(.+)$/', $name, $match))
  1959. return array($name);
  1960. $name = $match[1];
  1961. $secure = $match[2] == '##';
  1962. $trip = $match[3];
  1963. // convert to SHIT_JIS encoding
  1964. $trip = mb_convert_encoding($trip, 'Shift_JIS', 'UTF-8');
  1965. // generate salt
  1966. $salt = substr($trip . 'H..', 1, 2);
  1967. $salt = preg_replace('/[^.-z]/', '.', $salt);
  1968. $salt = strtr($salt, ':;<=>?@[\]^_`', 'ABCDEFGabcdef');
  1969. if ($secure) {
  1970. if (isset($config['custom_tripcode']["##{$trip}"]))
  1971. $trip = $config['custom_tripcode']["##{$trip}"];
  1972. else
  1973. $trip = '!!' . substr(crypt($trip, str_replace('+', '.', '_..A.' . substr(base64_encode(sha1($trip . $config['secure_trip_salt'], true)), 0, 4))), -10);
  1974. } else {
  1975. if (isset($config['custom_tripcode']["#{$trip}"]))
  1976. $trip = $config['custom_tripcode']["#{$trip}"];
  1977. else
  1978. $trip = '!' . substr(crypt($trip, $salt), -10);
  1979. }
  1980. return array($name, $trip);
  1981. }
  1982. // Highest common factor
  1983. function hcf($a, $b){
  1984. $gcd = 1;
  1985. if ($a>$b) {
  1986. $a = $a+$b;
  1987. $b = $a-$b;
  1988. $a = $a-$b;
  1989. }
  1990. if ($b==(round($b/$a))*$a)
  1991. $gcd=$a;
  1992. else {
  1993. for ($i=round($a/2);$i;$i--) {
  1994. if ($a == round($a/$i)*$i && $b == round($b/$i)*$i) {
  1995. $gcd = $i;
  1996. $i = false;
  1997. }
  1998. }
  1999. }
  2000. return $gcd;
  2001. }
  2002. function fraction($numerator, $denominator, $sep) {
  2003. $gcf = hcf($numerator, $denominator);
  2004. $numerator = $numerator / $gcf;
  2005. $denominator = $denominator / $gcf;
  2006. return "{$numerator}{$sep}{$denominator}";
  2007. }
  2008. function getPostByHash($hash) {
  2009. global $board;
  2010. $query = prepare(sprintf("SELECT `id`,`thread` FROM ``posts_%s`` WHERE `filehash` = :hash", $board['uri']));
  2011. $query->bindValue(':hash', $hash, PDO::PARAM_STR);
  2012. $query->execute() or error(db_error($query));
  2013. if ($post = $query->fetch(PDO::FETCH_ASSOC)) {
  2014. return $post;
  2015. }
  2016. return false;
  2017. }
  2018. function getPostByHashInThread($hash, $thread) {
  2019. global $board;
  2020. $query = prepare(sprintf("SELECT `id`,`thread` FROM ``posts_%s`` WHERE `filehash` = :hash AND ( `thread` = :thread OR `id` = :thread )", $board['uri']));
  2021. $query->bindValue(':hash', $hash, PDO::PARAM_STR);
  2022. $query->bindValue(':thread', $thread, PDO::PARAM_INT);
  2023. $query->execute() or error(db_error($query));
  2024. if ($post = $query->fetch(PDO::FETCH_ASSOC)) {
  2025. return $post;
  2026. }
  2027. return false;
  2028. }
  2029. function undoImage(array $post) {
  2030. if (!$post['has_file'] || !isset($post['files']))
  2031. return;
  2032. foreach ($post['files'] as $key => $file) {
  2033. if (isset($file['file_path']))
  2034. file_unlink($file['file_path']);
  2035. if (isset($file['thumb_path']))
  2036. file_unlink($file['thumb_path']);
  2037. }
  2038. }
  2039. function rDNS($ip_addr) {
  2040. global $config;
  2041. if ($config['cache']['enabled'] && ($host = cache::get('rdns_' . $ip_addr))) {
  2042. return $host;
  2043. }
  2044. if (!$config['dns_system']) {
  2045. $host = gethostbyaddr($ip_addr);
  2046. } else {
  2047. $resp = shell_exec_error('host -W 3 ' . $ip_addr);
  2048. if (preg_match('/domain name pointer ([^\s]+)$/', $resp, $m))
  2049. $host = $m[1];
  2050. else
  2051. $host = $ip_addr;
  2052. }
  2053. $isip = filter_var($host, FILTER_VALIDATE_IP);
  2054. if ($config['fcrdns'] && !$isip && DNS($host) != $ip_addr) {
  2055. $host = $ip_addr;
  2056. }
  2057. if ($config['cache']['enabled'])
  2058. cache::set('rdns_' . $ip_addr, $host);
  2059. return $host;
  2060. }
  2061. function DNS($host) {
  2062. global $config;
  2063. if ($config['cache']['enabled'] && ($ip_addr = cache::get('dns_' . $host))) {
  2064. return $ip_addr != '?' ? $ip_addr : false;
  2065. }
  2066. if (!$config['dns_system']) {
  2067. $ip_addr = gethostbyname($host);
  2068. if ($ip_addr == $host)
  2069. $ip_addr = false;
  2070. } else {
  2071. $resp = shell_exec_error('host -W 1 ' . $host);
  2072. if (preg_match('/has address ([^\s]+)$/', $resp, $m))
  2073. $ip_addr = $m[1];
  2074. else
  2075. $ip_addr = false;
  2076. }
  2077. if ($config['cache']['enabled'])
  2078. cache::set('dns_' . $host, $ip_addr !== false ? $ip_addr : '?');
  2079. return $ip_addr;
  2080. }
  2081. function shell_exec_error($command, $suppress_stdout = false) {
  2082. global $config, $debug;
  2083. if ($config['debug'])
  2084. $start = microtime(true);
  2085. $return = trim(shell_exec('PATH="' . escapeshellcmd($config['shell_path']) . ':$PATH";' .
  2086. $command . ' 2>&1 ' . ($suppress_stdout ? '> /dev/null ' : '') . '&& echo "TB_SUCCESS"'));
  2087. $return = preg_replace('/TB_SUCCESS$/', '', $return);
  2088. if ($config['debug']) {
  2089. $time = microtime(true) - $start;
  2090. $debug['exec'][] = array(
  2091. 'command' => $command,
  2092. 'time' => '~' . round($time * 1000, 2) . 'ms',
  2093. 'response' => $return ? $return : null
  2094. );
  2095. $debug['time']['exec'] += $time;
  2096. }
  2097. return $return === 'TB_SUCCESS' ? false : $return;
  2098. }
  2099. /* Die rolling:
  2100. * If "dice XdY+/-Z" is in the email field (where X or +/-Z may be
  2101. * missing), X Y-sided dice are rolled and summed, with the modifier Z
  2102. * added on. The result is displayed at the top of the post.
  2103. */
  2104. function diceRoller($post) {
  2105. global $config;
  2106. if(strpos(strtolower($post->email), 'dice%20') === 0) {
  2107. $dicestr = str_split(substr($post->email, strlen('dice%20')));
  2108. // Get params
  2109. $diceX = '';
  2110. $diceY = '';
  2111. $diceZ = '';
  2112. $curd = 'diceX';
  2113. for($i = 0; $i < count($dicestr); $i ++) {
  2114. if(is_numeric($dicestr[$i])) {
  2115. $$curd .= $dicestr[$i];
  2116. } else if($dicestr[$i] == 'd') {
  2117. $curd = 'diceY';
  2118. } else if($dicestr[$i] == '-' || $dicestr[$i] == '+') {
  2119. $curd = 'diceZ';
  2120. $$curd = $dicestr[$i];
  2121. }
  2122. }
  2123. // Default values for X and Z
  2124. if($diceX == '') {
  2125. $diceX = '1';
  2126. }
  2127. if($diceZ == '') {
  2128. $diceZ = '+0';
  2129. }
  2130. // Intify them
  2131. $diceX = intval($diceX);
  2132. $diceY = intval($diceY);
  2133. $diceZ = intval($diceZ);
  2134. // Continue only if we have valid values
  2135. if($diceX > 0 && $diceY > 0) {
  2136. $dicerolls = array();
  2137. $dicesum = $diceZ;
  2138. for($i = 0; $i < $diceX; $i++) {
  2139. $roll = rand(1, $diceY);
  2140. $dicerolls[] = $roll;
  2141. $dicesum += $roll;
  2142. }
  2143. // Prepend the result to the post body
  2144. $modifier = ($diceZ != 0) ? ((($diceZ < 0) ? ' - ' : ' + ') . abs($diceZ)) : '';
  2145. $dicesum = ($diceX > 1) ? ' = ' . $dicesum : '';
  2146. $post->body = '<table class="diceroll"><tr><td><img src="'.$config['dir']['static'].'d10.svg" alt="Dice roll" width="24"></td><td>Rolled ' . implode(', ', $dicerolls) . $modifier . $dicesum . '</td></tr></table><br/>' . $post->body;
  2147. }
  2148. }
  2149. }
  2150. function slugify($post) {
  2151. global $config;
  2152. $slug = "";
  2153. if (isset($post['subject']) && $post['subject'])
  2154. $slug = $post['subject'];
  2155. elseif (isset ($post['body_nomarkup']) && $post['body_nomarkup'])
  2156. $slug = $post['body_nomarkup'];
  2157. elseif (isset ($post['body']) && $post['body'])
  2158. $slug = strip_tags($post['body']);
  2159. // Fix UTF-8 first
  2160. $slug = mb_convert_encoding($slug, "UTF-8", "UTF-8");
  2161. // Transliterate local characters like ü, I wonder how would it work for weird alphabets :^)
  2162. $slug = iconv("UTF-8", "ASCII//TRANSLIT//IGNORE", $slug);
  2163. // Remove Tinyboard custom markup
  2164. $slug = preg_replace("/<tinyboard [^>]+>.*?<\/tinyboard>/s", '', $slug);
  2165. // Downcase everything
  2166. $slug = strtolower($slug);
  2167. // Strip bad characters, alphanumerics should suffice
  2168. $slug = preg_replace('/[^a-zA-Z0-9]/', '-', $slug);
  2169. // Replace multiple dashes with single ones
  2170. $slug = preg_replace('/-+/', '-', $slug);
  2171. // Strip dashes at the beginning and at the end
  2172. $slug = preg_replace('/^-|-$/', '', $slug);
  2173. // Slug should be X characters long, at max (80?)
  2174. $slug = substr($slug, 0, $config['slug_max_size']);
  2175. // Slug is now ready
  2176. return $slug;
  2177. }
  2178. function link_for($post, $page50 = false, $foreignlink = false, $thread = false) {
  2179. global $config, $board;
  2180. $post = (array)$post;
  2181. // Where do we need to look for OP?
  2182. $b = $foreignlink ? $foreignlink : (isset($post['board']) ? array('uri' => $post['board']) : $board);
  2183. $id = (isset($post['thread']) && $post['thread']) ? $post['thread'] : $post['id'];
  2184. $slug = false;
  2185. if ($config['slugify'] && ( (isset($post['thread']) && $post['thread']) || !isset ($post['slug']) ) ) {
  2186. $cvar = "slug_".$b['uri']."_".$id;
  2187. if (!$thread) {
  2188. $slug = Cache::get($cvar);
  2189. if ($slug === false) {
  2190. $query = prepare(sprintf("SELECT `slug` FROM ``posts_%s`` WHERE `id` = :id", $b['uri']));
  2191. $query->bindValue(':id', $id, PDO::PARAM_INT);
  2192. $query->execute() or error(db_error($query));
  2193. $thread = $query->fetch(PDO::FETCH_ASSOC);
  2194. $slug = $thread['slug'];
  2195. Cache::set($cvar, $slug);
  2196. }
  2197. }
  2198. else {
  2199. $slug = $thread['slug'];
  2200. }
  2201. }
  2202. elseif ($config['slugify']) {
  2203. $slug = $post['slug'];
  2204. }
  2205. if ( $page50 && $slug) $tpl = $config['file_page50_slug'];
  2206. else if (!$page50 && $slug) $tpl = $config['file_page_slug'];
  2207. else if ( $page50 && !$slug) $tpl = $config['file_page50'];
  2208. else if (!$page50 && !$slug) $tpl = $config['file_page'];
  2209. return sprintf($tpl, $id, $slug);
  2210. }
  2211. function prettify_textarea($s){
  2212. return str_replace("\t", '&#09;', str_replace("\n", '&#13;&#10;', htmlentities($s)));
  2213. }
  2214. /*class HTMLPurifier_URIFilter_NoExternalImages extends HTMLPurifier_URIFilter {
  2215. public $name = 'NoExternalImages';
  2216. public function filter(&$uri, $c, $context) {
  2217. global $config;
  2218. $ct = $context->get('CurrentToken');
  2219. if (!$ct || $ct->name !== 'img') return true;
  2220. if (!isset($uri->host) && !isset($uri->scheme)) return true;
  2221. if (!in_array($uri->scheme . '://' . $uri->host . '/', $config['allowed_offsite_urls'])) {
  2222. error('No off-site links in board announcement images.');
  2223. }
  2224. return true;
  2225. }
  2226. }*/
  2227. function purify_html($s) {
  2228. global $config;
  2229. $c = HTMLPurifier_Config::createDefault();
  2230. $c->set('HTML.Allowed', $config['allowed_html']);
  2231. $uri = $c->getDefinition('URI');
  2232. $uri->addFilter(new HTMLPurifier_URIFilter_NoExternalImages(), $c);
  2233. $purifier = new HTMLPurifier($c);
  2234. $clean_html = $purifier->purify($s);
  2235. return $clean_html;
  2236. }
  2237. function markdown($s) {
  2238. $pd = new Parsedown();
  2239. $pd->setMarkupEscaped(true);
  2240. $pd->setimagesEnabled(false);
  2241. return $pd->text($s);
  2242. }
  2243. function generation_strategy($fun, $array=array()) { global $config;
  2244. $action = false;
  2245. foreach ($config['generation_strategies'] as $s) {
  2246. if ($action = $s($fun, $array)) {
  2247. break;
  2248. }
  2249. }
  2250. switch ($action[0]) {
  2251. case 'immediate':
  2252. return 'rebuild';
  2253. case 'defer':
  2254. // Ok, it gets interesting here :)
  2255. get_queue('generate')->push(serialize(array('build', $fun, $array, $action)));
  2256. return 'ignore';
  2257. case 'build_on_load':
  2258. return 'delete';
  2259. }
  2260. }
  2261. function strategy_immediate($fun, $array) {
  2262. return array('immediate');
  2263. }
  2264. function strategy_smart_build($fun, $array) {
  2265. return array('build_on_load');
  2266. }
  2267. function strategy_sane($fun, $array) { global $config;
  2268. if (php_sapi_name() == 'cli') return false;
  2269. else if (isset($_POST['mod'])) return false;
  2270. // Thread needs to be done instantly. Same with a board page, but only if posting a new thread.
  2271. else if ($fun == 'sb_thread' || ($fun == 'sb_board' && $array[1] == 1 && isset ($_POST['page']))) return array('immediate');
  2272. else return false;
  2273. }
  2274. // My first, test strategy.
  2275. function strategy_first($fun, $array) {
  2276. switch ($fun) {
  2277. case 'sb_thread':
  2278. return array('defer');
  2279. case 'sb_board':
  2280. if ($array[1] > 8) return array('build_on_load');
  2281. else return array('defer');
  2282. case 'sb_api':
  2283. return array('defer');
  2284. case 'sb_catalog':
  2285. return array('defer');
  2286. case 'sb_recent':
  2287. return array('build_on_load');
  2288. case 'sb_sitemap':
  2289. return array('build_on_load');
  2290. case 'sb_ukko':
  2291. return array('defer');
  2292. }
  2293. }