The version of vichan running on lainchan.org
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.

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