diff options
| author | Benjamin Southall <applemansigloo@gmail.com> | 2016-09-25 16:34:47 +0900 |
|---|---|---|
| committer | Benjamin Southall <applemansigloo@gmail.com> | 2016-09-25 16:34:47 +0900 |
| commit | a3b9c4405c6d4cca2a70dbb400e3ba3f65e7ea04 (patch) | |
| tree | 5a2ebdb801748586e3fde7c12cbeda169b2c3e31 /inc | |
| parent | 9436524a306c8e3d6d8a6ed195e2a3c56d5cde39 (diff) | |
| parent | a4b619ae5eaab5755dc215a496854dee8f7ce395 (diff) | |
Merged lainchan with vichan master at 25/9/2016 a4b619ae5eaab5755dc215a496854dee8f7ce395 @czaks czaks committed on GitHub 2 days ago
Diffstat (limited to 'inc')
| -rw-r--r-- | inc/api.php | 8 | ||||
| -rw-r--r-- | inc/bans.php | 2 | ||||
| -rw-r--r-- | inc/config.php | 202 | ||||
| -rw-r--r-- | inc/controller.php | 108 | ||||
| -rw-r--r-- | inc/display.php | 58 | ||||
| -rwxr-xr-x | inc/functions.php | 277 | ||||
| -rw-r--r-- | inc/lib/Twig/Extensions/Extension/Tinyboard.php | 1 | ||||
| -rw-r--r-- | inc/lock.php | 39 | ||||
| -rw-r--r-- | inc/mod/auth.php | 143 | ||||
| -rw-r--r-- | inc/mod/pages.php | 277 | ||||
| -rw-r--r-- | inc/nntpchan/nntpchan.php | 152 | ||||
| -rw-r--r-- | inc/nntpchan/tests.php | 30 | ||||
| -rw-r--r-- | inc/polyfill.php | 28 | ||||
| -rw-r--r-- | inc/queue.php | 49 | ||||
| -rw-r--r-- | inc/route.php | 65 |
15 files changed, 1285 insertions, 154 deletions
diff --git a/inc/api.php b/inc/api.php index 57e4d367..b280c25b 100644 --- a/inc/api.php +++ b/inc/api.php @@ -32,6 +32,7 @@ class Api { 'images' => 'images', 'sticky' => 'sticky', 'locked' => 'locked', + 'cycle' => 'cyclical', 'bump' => 'last_modified', 'embed' => 'embed', ); @@ -92,7 +93,12 @@ class Api { $dotPos = strrpos($file->file, '.'); $apiPost['ext'] = substr($file->file, $dotPos); $apiPost['tim'] = substr($file->file, 0, $dotPos); - $apiPost['md5'] = base64_encode(hex2bin($post->filehash)); + if (isset ($file->hash) && $file->hash) { + $apiPost['md5'] = base64_encode(hex2bin($file->hash)); + } + else if (isset ($post->filehash) && $post->filehash) { + $apiPost['md5'] = base64_encode(hex2bin($post->filehash)); + } } private function translatePost($post, $threadsPage = false) { diff --git a/inc/bans.php b/inc/bans.php index e28aec0a..7a514f04 100644 --- a/inc/bans.php +++ b/inc/bans.php @@ -174,7 +174,7 @@ class Bans { if ($ban['post'] && !$hide_message) { $post = json_decode($ban['post']); - $ban['message'] = $post->body; + $ban['message'] = isset($post->body) ? $post->body : 0; } unset($ban['ipstart'], $ban['ipend'], $ban['post'], $ban['creator']); diff --git a/inc/config.php b/inc/config.php index 86ddd5aa..dc65068f 100644 --- a/inc/config.php +++ b/inc/config.php @@ -103,7 +103,7 @@ /* * ==================== - * Cache settings + * Cache, lock and queue settings * ==================== */ @@ -120,6 +120,7 @@ // $config['cache']['enabled'] = 'apc'; // $config['cache']['enabled'] = 'memcached'; // $config['cache']['enabled'] = 'redis'; + // $config['cache']['enabled'] = 'fs'; // Timeout for cached objects such as posts and HTML. $config['cache']['timeout'] = 60 * 60 * 48; // 48 hours @@ -142,6 +143,12 @@ // (this file will be explicitly loaded during cache hit, but not during cache miss). $config['cache_config'] = false; + // Define a lock driver. + $config['lock']['enabled'] = 'fs'; + + // Define a queue driver. + $config['queue']['enabled'] = 'fs'; // xD + /* * ==================== * Cookie settings @@ -511,6 +518,13 @@ // The timeout for the above, in seconds. $config['upload_by_url_timeout'] = 15; + // Enable early 404? With default settings, a thread would 404 if it was to leave page 3, if it had less + // than 3 replies. + $config['early_404'] = false; + + $config['early_404_page'] = 3; + $config['early_404_replies'] = 5; + // A wordfilter (sometimes referred to as just a "filter" or "censor") automatically scans users’ posts // as they are submitted and changes or censors particular words or phrases. @@ -550,6 +564,9 @@ // When true, the sage won't be displayed $config['hide_sage'] = false; + // Don't display user's email when it's not "sage" + $config['hide_email'] = false; + // Attach country flags to posts. $config['country_flags'] = false; @@ -763,7 +780,7 @@ // Location of thumbnail to use for spoiler images. $config['spoiler_image'] = 'static/spoiler.png'; // Location of thumbnail to use for deleted images. - // $config['image_deleted'] = 'static/deleted.png'; + $config['image_deleted'] = 'static/deleted.png'; // When a thumbnailed image is going to be the same (in dimension), just copy the entire file and use // that as a thumbnail instead of resizing/redrawing. @@ -804,8 +821,17 @@ // Set this to true if you're using a BSD $config['bsd_md5'] = false; - // Set this to true if you're having problems with image duplicated error and bsd_md5 doesn't help. - $config['php_md5'] = false; + // Set this to true if you're using Linux and you can execute `md5sum` binary. + $config['gnu_md5'] = false; + + // Use Tesseract OCR to retrieve text from images, so you can use it as a spamfilter. + $config['tesseract_ocr'] = false; + + // Tesseract parameters + $config['tesseract_params'] = ''; + + // Tesseract preprocess command + $config['tesseract_preprocess_command'] = 'convert -monochrome %s -'; // Number of posts in a "View Last X Posts" page $config['noko50_count'] = 50; @@ -928,8 +954,8 @@ // Show page navigation links at the top as well. $config['page_nav_top'] = false; - // Show "Catalog" link in page navigation. Use with the Catalog theme. - // $config['catalog_link'] = 'catalog.html'; + // Show "Catalog" link in page navigation. Use with the Catalog theme. Set to false to disable. + $config['catalog_link'] = 'catalog.html'; // Board categories. Only used in the "Categories" theme. // $config['categories'] = array( @@ -998,6 +1024,10 @@ // Minify Javascript using http://code.google.com/p/minify/. $config['minify_js'] = false; + // Dispatch thumbnail loading and image configuration with JavaScript. It will need a certain javascript + // code to work. + $config['javascript_image_dispatch'] = false; + /* * ==================== * Video embedding @@ -1193,16 +1223,74 @@ // Try not to build pages when we shouldn't have to. $config['try_smarter'] = true; - // EXPERIMENTAL: Defer static HTML building to a moment, when a given file is actually accessed. - // Warning: This option won't run out of the box. You need to tell your webserver, that a file - // for serving 403 and 404 pages is /smart_build.php. Also, you need to turn off indexes. +/* + * ==================== + * Advanced build + * ==================== + */ + + // Strategies for file generation. Also known as an "advanced build". If you don't have performance + // issues, you can safely ignore that part, because it's hard to configure and won't even work on + // your free webhosting. + // + // A strategy is a function, that given the PHP environment and ($fun, $array) variable pair, returns + // an $action array or false. + // + // $fun - a controller function name, see inc/controller.php. This is named after functions, so that + // we can generate the files in daemon. + // + // $array - arguments to be passed + // + // $action - action to be taken. It's an array, and the first element of it is one of the following: + // * "immediate" - generate the page immediately + // * "defer" - defer page generation to a moment a worker daemon gets to build it (serving a stale + // page in the meantime). The remaining arguments are daemon-specific. Daemon isn't + // implemented yet :DDDD inb4 while(true) { generate(Queue::Get()) }; (which is probably it). + // * "build_on_load" - defer page generation to a moment, when the user actually accesses the page. + // This is a smart_build behaviour. You shouldn't use this one too much, if you + // use it for active boards, the server may choke due to a possible race condition. + // See my blog post: https://engine.vichan.net/blog/res/2.html + // + // So, let's assume we want to build a thread 1324 on board /b/, because a new post appeared there. + // We try the first strategy, giving it arguments: 'sb_thread', array('b', 1324). The strategy will + // now return a value $action, denoting an action to do. If $action is false, we try another strategy. + // + // As I said, configuration isn't easy. + // + // 1. chmod 0777 directories: tmp/locks/ and tmp/queue/. + // 2. serve 403 and 404 requests to go thru smart_build.php + // for nginx, this blog post contains config snippets: https://engine.vichan.net/blog/res/2.html + // 3. disable indexes in your webserver + // 4. launch any number of daemons (eg. twice your number of threads?) using the command: + // $ tools/worker.php + // You don't need to do that step if you are not going to use the "defer" option. + // 5. enable smart_build_helper (see below) + // 6. edit the strategies (see inc/functions.php for the builtin ones). You can use lambdas. I will test + // various ones and include one that works best for me. + $config['generation_strategies'] = array(); + // Add a sane strategy. It forces to immediately generate a page user is about to land on. Otherwise, + // it has no opinion, so it needs a fallback strategy. + $config['generation_strategies'][] = 'strategy_sane'; + // Add an immediate catch-all strategy. This is the default function of imageboards: generate all pages + // on post time. + $config['generation_strategies'][] = 'strategy_immediate'; + // NOT RECOMMENDED: Instead of an all-"immediate" strategy, you can use an all-"build_on_load" one (used + // to be initialized using $config['smart_build']; ) for all pages instead of those to be build + // immediately. A rebuild done in this mode should remove all your static files + // $config['generation_strategies'][1] = 'strategy_smart_build'; + + // Deprecated. Leave it false. See above. $config['smart_build'] = false; - // Smart build related: when a file doesn't exist, where should we redirect? + // Use smart_build.php for dispatching missing requests. It may be useful without smart_build or advanced + // build, for example it will regenerate the missing files. + $config['smart_build_helper'] = true; + + // smart_build.php: when a file doesn't exist, where should we redirect? $config['page_404'] = '/404.html'; - // Smart build related: extra entrypoints. - $config['smart_build_entrypoints'] = array(); + // Extra controller entrypoints. Controller is used only by smart_build and advanced build. + $config['controller_entrypoints'] = array(); /* * ==================== @@ -1235,6 +1323,8 @@ $config['mod']['link_bumpunlock'] = '[-Sage]'; $config['mod']['link_editpost'] = '[Edit]'; $config['mod']['link_move'] = '[Move]'; + $config['mod']['link_cycle'] = '[Cycle]'; + $config['mod']['link_uncycle'] = '[-Cycle]'; // Moderator capcodes. $config['capcode'] = ' <span class="capcode">## %s</span>'; @@ -1378,6 +1468,9 @@ $config['mod']['deletebyip_global'] = ADMIN; // Sticky a thread $config['mod']['sticky'] = MOD; + // Cycle a thread + $config['mod']['cycle'] = MOD; + $config['cycle_limit'] = &$config['reply_limit']; // Lock a thread $config['mod']['lock'] = MOD; // Post in a locked thread @@ -1488,6 +1581,9 @@ $config['mod']['ban_appeals'] = MOD; // View the recent posts page $config['mod']['recent'] = MOD; + // Create pages + $config['mod']['edit_pages'] = MOD; + $config['pages_max'] = 10; // Config editor permissions $config['mod']['config'] = array(); @@ -1534,25 +1630,30 @@ /* * ==================== - * Public post search + * Public pages * ==================== */ + + // Public post search settings $config['search'] = array(); // Enable the search form $config['search']['enable'] = false; // Maximal number of queries per IP address per minutes - $config['search']['queries_per_minutes'] = Array(15, 2); + $config['search']['queries_per_minutes'] = Array(15, 2); // Global maximal number of queries per minutes - $config['search']['queries_per_minutes_all'] = Array(50, 2); + $config['search']['queries_per_minutes_all'] = Array(50, 2); // Limit of search results - $config['search']['search_limit'] = 100; + $config['search']['search_limit'] = 100; // Boards for searching - //$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e'); + //$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e'); + + // Enable public logs? 0: NO, 1: YES, 2: YES, but drop names + $config['public_logs'] = 0; /* * ==================== @@ -1589,6 +1690,45 @@ // $config['api']['extra_fields'] = array('body_nomarkup' => 'com_nomarkup'); /* + * ================== + * NNTPChan settings + * ================== + */ + +/* + * Please keep in mind that NNTPChan support in vichan isn't finished yet / is in an experimental + * state. Please join #nntpchan on Rizon in order to peer with someone. + */ + + $config['nntpchan'] = array(); + + // Enable NNTPChan integration + $config['nntpchan']['enabled'] = false; + + // NNTP server + $config['nntpchan']['server'] = "localhost:1119"; + + // Global dispatch array. Add your boards to it to enable them. Please make + // sure that this setting is set in a global context. + $config['nntpchan']['dispatch'] = array(); // 'overchan.test' => 'test' + + // Trusted peer - an IP address of your NNTPChan instance. This peer will have + // increased capabilities, eg.: will evade spamfilter. + $config['nntpchan']['trusted_peer'] = '127.0.0.1'; + + // Salt for message ID generation. Keep it long and secure. + $config['nntpchan']['salt'] = 'change_me+please'; + + // A local message ID domain. Make sure to change it. + $config['nntpchan']['domain'] = 'example.vichan.net'; + + // An NNTPChan group name. + // Please set this setting in your board/config.php, not globally. + $config['nntpchan']['group'] = false; // eg. 'overchan.test' + + + +/* * ==================== * Other/uncategorized * ==================== @@ -1670,8 +1810,28 @@ '<img style="width:360px;height:270px;" src="//img.youtube.com/vi/$2/0.jpg" class="post-image"/>'. '</a></div>'; - // Slack Report Notification - $config['slack'] = true; - $config['slack_channel'] = "reports"; - $config['slack_incoming_webhook_endpoint'] = "https://hooks.slack.com/services/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"; + // Slack Report Notification + $config['slack'] = true; + $config['slack_channel'] = "reports"; + $config['slack_incoming_webhook_endpoint'] = "https://hooks.slack.com/services/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"; + + // Password hashing function + // + // $5$ <- SHA256 + // $6$ <- SHA512 + // + // 25000 rounds make for ~0.05s on my 2015 Core i3 computer. + // + // https://secure.php.net/manual/en/function.crypt.php + $config['password_crypt'] = '$6$rounds=25000$'; + + // Password hashing method version + // If set to 0, it won't upgrade hashes using old password encryption schema, only create new. + // You can set it to a higher value, to further migrate to other password hashing function. + $config['password_crypt_version'] = 1; + + // Use CAPTCHA for reports? + $config['report_captcha'] = false; + // Allowed HTML tags in ?/edit_pages. + $config['allowed_html'] = 'a[href|title],p,br,li,ol,ul,strong,em,u,h2,b,i,tt,div,img[src|alt|title],hr'; diff --git a/inc/controller.php b/inc/controller.php new file mode 100644 index 00000000..02e33443 --- /dev/null +++ b/inc/controller.php @@ -0,0 +1,108 @@ +<?php + +// This file contains the controller part of vichan + +// don't bother with that unless you use smart build or advanced build +// you can use those parts for your own implementations though :^) + +defined('TINYBOARD') or exit; + +function sb_board($b, $page = 1) { global $config, $build_pages; $page = (int)$page; + if ($page < 1) return false; + if (!openBoard($b)) return false; + if ($page > $config['max_pages']) return false; + $config['try_smarter'] = true; + $build_pages = array($page); + buildIndex("skip"); + return true; +} + +function sb_api_board($b, $page = 0) { $page = (int)$page; + return sb_board($b, $page + 1); +} + +function sb_thread($b, $thread, $slugcheck = false) { global $config; $thread = (int)$thread; + if ($thread < 1) return false; + + if (!preg_match('/^'.$config['board_regex'].'$/u', $b)) return false; + + if (Cache::get("thread_exists_".$b."_".$thread) == "no") return false; + + $query = prepare(sprintf("SELECT MAX(`id`) AS `max` FROM ``posts_%s``", $b)); + if (!$query->execute()) return false; + + $s = $query->fetch(PDO::FETCH_ASSOC); + $max = $s['max']; + + if ($thread > $max) return false; + + $query = prepare(sprintf("SELECT `id` FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL", $b)); + $query->bindValue(':id', $thread); + + if (!$query->execute() || !$query->fetch(PDO::FETCH_ASSOC) ) { + Cache::set("thread_exists_".$b."_".$thread, "no", 3600); + return false; + } + + if ($slugcheck && $config['slugify']) { + global $request; + + $link = link_for(array("id" => $thread), $slugcheck === 50, array("uri" => $b)); + $link = "/".$b."/".$config['dir']['res'].$link; + + if ($link != $request) { + header("Location: $link", true, 301); + die(); + } + } + + if ($slugcheck == 50) { // Should we really generate +50 page? Maybe there are not enough posts anyway + global $request; + $r = str_replace("+50", "", $request); + $r = substr($r, 1); // Cut the slash + + if (file_exists($r)) return false; + } + + if (!openBoard($b)) return false; + buildThread($thread); + return true; +} + +function sb_thread_slugcheck($b, $thread) { + return sb_thread($b, $thread, true); +} +function sb_thread_slugcheck50($b, $thread) { + return sb_thread($b, $thread, 50); +} + +function sb_api($b) { global $config, $build_pages; + if (!openBoard($b)) return false; + $config['try_smarter'] = true; + $build_pages = array(-1); + buildIndex(); + return true; +} + +function sb_ukko() { + rebuildTheme("ukko", "post-thread"); + return true; +} + +function sb_catalog($b) { + if (!openBoard($b)) return false; + + rebuildTheme("catalog", "post-thread", $b); + return true; +} + +function sb_recent() { + rebuildTheme("recent", "post-thread"); + return true; +} + +function sb_sitemap() { + rebuildTheme("sitemap", "all"); + return true; +} + diff --git a/inc/display.php b/inc/display.php index ce39197c..58cb4805 100644 --- a/inc/display.php +++ b/inc/display.php @@ -71,6 +71,64 @@ function createBoardlist($mod=false) { ); } +function error($message, $priority = true, $debug_stuff = false) { + global $board, $mod, $config, $db_error; + + if ($config['syslog'] && $priority !== false) { + // Use LOG_NOTICE instead of LOG_ERR or LOG_WARNING because most error message are not significant. + _syslog($priority !== true ? $priority : LOG_NOTICE, $message); + } + + if (defined('STDIN')) { + // Running from CLI + echo('Error: ' . $message . "\n"); + debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + die(); + } + + if ($config['debug'] && isset($db_error)) { + $debug_stuff = array_combine(array('SQLSTATE', 'Error code', 'Error message'), $db_error); + } + + if ($config['debug']) { + $debug_stuff['backtrace'] = debug_backtrace(); + } + + if (isset($_POST['json_response'])) { + header('Content-Type: text/json; charset=utf-8'); + die(json_encode(array( + 'error' => $message + ))); + } + else { + header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); + } + + $pw = $config['db']['password']; + $debug_callback = function(&$item) use (&$debug_callback, $pw) { + if (is_array($item)) { + $item = array_filter($item, $debug_callback); + } + return ($item !== $pw || !$pw); + }; + + + if ($debug_stuff) + $debug_stuff = array_filter($debug_stuff, $debug_callback); + + die(Element('page.html', array( + 'config' => $config, + 'title' => _('Error'), + 'subtitle' => _('An error has occured.'), + 'body' => Element('error.html', array( + 'config' => $config, + 'message' => $message, + 'mod' => $mod, + 'board' => isset($board) ? $board : false, + 'debug' => is_array($debug_stuff) ? str_replace("\n", ' ', utf8tohtml(print_r($debug_stuff, true))) : utf8tohtml($debug_stuff) + )) + ))); +} function loginForm($error=false, $username=false, $redirect=false) { global $config; diff --git a/inc/functions.php b/inc/functions.php index 5213c8bd..86efe789 100755 --- a/inc/functions.php +++ b/inc/functions.php @@ -19,6 +19,12 @@ require_once 'inc/template.php'; require_once 'inc/database.php'; require_once 'inc/events.php'; require_once 'inc/api.php'; +require_once 'inc/mod/auth.php'; +require_once 'inc/lock.php'; +require_once 'inc/queue.php'; +require_once 'inc/polyfill.php'; +@include_once 'inc/lib/parsedown/Parsedown.php'; // fail silently, this isn't a critical piece of code + if (!extension_loaded('gettext')) { require_once 'inc/lib/gettext/gettext.inc'; } @@ -86,6 +92,8 @@ function loadConfig() { 'db', 'api', 'cache', + 'lock', + 'queue', 'cookies', 'error', 'dir', @@ -123,7 +131,7 @@ function loadConfig() { // So, we may store the locale in a tmp/ filesystem. if (file_exists($fn = 'tmp/cache/locale_' . $boardsuffix ) ) { - $config['locale'] = file_get_contents($fn); + $config['locale'] = @file_get_contents($fn); } else { $config['locale'] = 'en'; @@ -134,13 +142,13 @@ function loadConfig() { $configstr .= file_get_contents($board['dir'] . '/config.php'); } $matches = array(); - preg_match_all('/[^\/*#]\$config\s*\[\s*[\'"]locale[\'"]\s*\]\s*=\s*([\'"])(.*?)\1/', $configstr, $matches); + preg_match_all('/[^\/#*]\$config\s*\[\s*[\'"]locale[\'"]\s*\]\s*=\s*([\'"])(.*?)\1/', $configstr, $matches); if ($matches && isset ($matches[2]) && $matches[2]) { $matches = $matches[2]; $config['locale'] = $matches[count($matches)-1]; } - file_put_contents($fn, $config['locale']); + @file_put_contents($fn, $config['locale']); } if ($config['locale'] != $current_locale) { @@ -480,7 +488,8 @@ function setupBoard($array) { $board = array( 'uri' => $array['uri'], 'title' => $array['title'], - 'subtitle' => $array['subtitle'] + 'subtitle' => $array['subtitle'], + #'indexed' => $array['indexed'], ); // older versions @@ -505,14 +514,19 @@ function setupBoard($array) { } function openBoard($uri) { - global $config, $build_pages; + global $config, $build_pages, $board; if ($config['try_smarter']) $build_pages = array(); - $board = getBoardInfo($uri); - if ($board) { - setupBoard($board); + // And what if we don't really need to change a board we have opened? + if (isset ($board) && isset ($board['uri']) && $board['uri'] == $uri) { + return true; + } + + $b = getBoardInfo($uri); + if ($b) { + setupBoard($b); if (function_exists('after_open_board')) { after_open_board(); @@ -836,7 +850,7 @@ function displayBan($ban) { Element('page.html', array( 'title' => _('Banned!'), 'config' => $config, - 'nojavascript' => true, + 'boardlist' => createBoardlist(isset($mod) ? $mod : false), 'body' => Element('banned.html', array( 'config' => $config, 'ban' => $ban, @@ -974,7 +988,7 @@ function insertFloodPost(array $post) { function post(array $post) { global $pdo, $board; - $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, 0, :embed, :slug)", $board['uri'])); + $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'])); // Basic stuff if (!empty($post['subject'])) { @@ -1014,6 +1028,12 @@ function post(array $post) { $query->bindValue(':locked', false, PDO::PARAM_INT); } + if ($post['op'] && $post['mod'] && isset($post['cycle']) && $post['cycle']) { + $query->bindValue(':cycle', true, PDO::PARAM_INT); + } else { + $query->bindValue(':cycle', false, PDO::PARAM_INT); + } + if ($post['mod'] && isset($post['capcode']) && $post['capcode']) { $query->bindValue(':capcode', $post['capcode'], PDO::PARAM_INT); } else { @@ -1086,6 +1106,8 @@ function deleteFile($id, $remove_entirely_if_already=true, $file=null) { $files = json_decode($post['files']); $file_to_delete = $file !== false ? $files[(int)$file] : (object)array('file' => false); + if (!$files[0]) error(_('That post has no files.')); + if ($files[0]->file == 'deleted' && $post['num_files'] == 1 && !$post['thread']) return; // Can't delete OP's image completely. @@ -1097,8 +1119,10 @@ function deleteFile($id, $remove_entirely_if_already=true, $file=null) { foreach ($files as $i => $f) { if (($file !== false && $i == $file) || $file === null) { // Delete thumbnail - file_unlink($board['dir'] . $config['dir']['thumb'] . $f->thumb); - unset($files[$i]->thumb); + if (isset ($f->thumb) && $f->thumb) { + file_unlink($board['dir'] . $config['dir']['thumb'] . $f->thumb); + unset($files[$i]->thumb); + } // Delete file file_unlink($board['dir'] . $config['dir']['img'] . $f->file); @@ -1120,19 +1144,22 @@ function deleteFile($id, $remove_entirely_if_already=true, $file=null) { // rebuild post (markup) function rebuildPost($id) { - global $board; + global $board, $mod; - $query = prepare(sprintf("SELECT `body_nomarkup`, `thread` FROM ``posts_%s`` WHERE `id` = :id", $board['uri'])); + $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE `id` = :id", $board['uri'])); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); if ((!$post = $query->fetch(PDO::FETCH_ASSOC)) || !$post['body_nomarkup']) return false; - markup($body = &$post['body_nomarkup']); + markup($post['body'] = &$post['body_nomarkup']); + $post = (object)$post; + event('rebuildpost', $post); + $post = (array)$post; $query = prepare(sprintf("UPDATE ``posts_%s`` SET `body` = :body WHERE `id` = :id", $board['uri'])); - $query->bindValue(':body', $body); + $query->bindValue(':body', $post['body']); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); @@ -1221,7 +1248,7 @@ function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) { return true; } -function clean() { +function clean($pid = false) { global $board, $config; $offset = round($config['max_pages']*$config['threads_per_page']); @@ -1232,6 +1259,22 @@ function clean() { $query->execute() or error(db_error($query)); while ($post = $query->fetch(PDO::FETCH_ASSOC)) { deletePost($post['id'], false, false); + if ($pid) modLog("Automatically deleting thread #{$post['id']} due to new thread #{$pid}"); + } + + // Bump off threads with X replies earlier, spam prevention method + if ($config['early_404']) { + $offset = round($config['early_404_page']*$config['threads_per_page']); + $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'])); + $query->bindValue(':offset', $offset, PDO::PARAM_INT); + $query->execute() or error(db_error($query)); + + while ($post = $query->fetch(PDO::FETCH_ASSOC)) { + if ($post['reply_count'] < $config['early_404_replies']) { + deletePost($post['thread_id'], false, false); + 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)"); + } + } } } @@ -1245,7 +1288,8 @@ function thread_find_page($thread) { return floor(($config['threads_per_page'] + $index) / $config['threads_per_page']); } -function index($page, $mod=false) { +// $brief means that we won't need to generate anything yet +function index($page, $mod=false, $brief = false) { global $board, $config, $debug; $body = ''; @@ -1276,6 +1320,7 @@ function index($page, $mod=false) { unset($cached); } } + if (!isset($cached)) { $posts = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE `thread` = :id ORDER BY `id` DESC LIMIT :limit", $board['uri'])); $posts->bindValue(':id', $th['id']); @@ -1315,7 +1360,10 @@ function index($page, $mod=false) { } $threads[] = $thread; - $body .= $thread->build(true); + + if (!$brief) { + $body .= $thread->build(true); + } } if ($config['file_board']) { @@ -1536,27 +1584,28 @@ function checkMute() { function buildIndex($global_api = "yes") { global $board, $config, $build_pages; - if (!$config['smart_build']) { - $pages = getPages(); - if (!$config['try_smarter']) - $antibot = create_antibot($board['uri']); + $catalog_api_action = generation_strategy('sb_api', array($board['uri'])); - if ($config['api']['enabled']) { - $api = new Api(); - $catalog = array(); - } + $pages = null; + $antibot = null; + + if ($config['api']['enabled']) { + $api = new Api(); + $catalog = array(); } for ($page = 1; $page <= $config['max_pages']; $page++) { $filename = $board['dir'] . ($page == 1 ? $config['file_index'] : sprintf($config['file_page'], $page)); $jsonFilename = $board['dir'] . ($page - 1) . '.json'; // pages should start from 0 - if ((!$config['api']['enabled'] || $global_api == "skip" || $config['smart_build']) && $config['try_smarter'] - && isset($build_pages) && !empty($build_pages) && !in_array($page, $build_pages) ) + $wont_build_this_page = $config['try_smarter'] && isset($build_pages) && !empty($build_pages) && !in_array($page, $build_pages); + + if ((!$config['api']['enabled'] || $global_api == "skip") && $wont_build_this_page) continue; - if (!$config['smart_build']) { - $content = index($page); + $action = generation_strategy('sb_board', array($board['uri'], $page)); + if ($action == 'rebuild' || $catalog_api_action == 'rebuild') { + $content = index($page, false, $wont_build_this_page); if (!$content) break; @@ -1567,17 +1616,21 @@ function buildIndex($global_api = "yes") { file_write($jsonFilename, $json); $catalog[$page-1] = $threads; - } - if ($config['api']['enabled'] && $global_api != "skip" && $config['try_smarter'] && isset($build_pages) - && !empty($build_pages) && !in_array($page, $build_pages) ) - continue; + if ($wont_build_this_page) continue; + } if ($config['try_smarter']) { $antibot = create_antibot($board['uri'], 0 - $page); $content['current_page'] = $page; } + elseif (!$antibot) { + $antibot = create_antibot($board['uri']); + } $antibot->reset(); + if (!$pages) { + $pages = getPages(); + } $content['pages'] = $pages; $content['pages'][$page-1]['selected'] = true; $content['btn'] = getPageButtons($content['pages']); @@ -1585,13 +1638,14 @@ function buildIndex($global_api = "yes") { file_write($filename, Element('index.html', $content)); } - else { + elseif ($action == 'delete' || $catalog_api_action == 'delete') { file_unlink($filename); file_unlink($jsonFilename); } } - if (!$config['smart_build'] && $page < $config['max_pages']) { + // $action is an action for our last page + if (($catalog_api_action == 'rebuild' || $action == 'rebuild' || $action == 'delete') && $page < $config['max_pages']) { for (;$page<=$config['max_pages'];$page++) { $filename = $board['dir'] . ($page==1 ? $config['file_index'] : sprintf($config['file_page'], $page)); file_unlink($filename); @@ -1605,13 +1659,13 @@ function buildIndex($global_api = "yes") { // json api catalog if ($config['api']['enabled'] && $global_api != "skip") { - if ($config['smart_build']) { + if ($catalog_api_action == 'delete') { $jsonFilename = $board['dir'] . 'catalog.json'; file_unlink($jsonFilename); $jsonFilename = $board['dir'] . 'threads.json'; file_unlink($jsonFilename); } - else { + elseif ($catalog_api_action == 'rebuild') { $json = json_encode($api->translateCatalog($catalog)); $jsonFilename = $board['dir'] . 'catalog.json'; file_write($jsonFilename, $json); @@ -1672,13 +1726,15 @@ function buildJavascript() { function checkDNSBL() { global $config; - if (isIPv6()) return; // No IPv6 support yet. if (!isset($_SERVER['REMOTE_ADDR'])) return; // Fix your web server configuration + if (preg_match("/^(::(ffff:)?)?(127\.|192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|0\.|255\.)/", $_SERVER['REMOTE_ADDR'])) + return; // It's pointless to check for local IP addresses in dnsbls, isn't it? + if (in_array($_SERVER['REMOTE_ADDR'], $config['dnsbl_exceptions'])) return; @@ -1811,7 +1867,11 @@ function extract_modifiers($body) { return $modifiers; } -function markup(&$body, $track_cites = false) { +function remove_modifiers($body) { + return preg_replace('@<tinyboard ([\w\s]+)>(.+?)</tinyboard>@usm', '', $body); +} + +function markup(&$body, $track_cites = false, $op = false) { global $board, $config, $markup_urls; $modifiers = extract_modifiers($body); @@ -1907,7 +1967,7 @@ function markup(&$body, $track_cites = false) { } if (isset($cited_posts[$cite])) { - $replacement = '<a onclick="highlightReply(\''.$cite.'\');" href="' . + $replacement = '<a onclick="highlightReply(\''.$cite.'\', event);" href="' . $config['root'] . $board['dir'] . $config['dir']['res'] . link_for(array('id' => $cite, 'thread' => $cited_posts[$cite])) . '#' . $cite . '">' . '>>' . $cite . @@ -2006,7 +2066,7 @@ function markup(&$body, $track_cites = false) { $replacement = '<a ' . ($_board == $board['uri'] ? - 'onclick="highlightReply(\''.$cite.'\');" ' + 'onclick="highlightReply(\''.$cite.'\', event);" ' : '') . 'href="' . $link . '">' . '>>>/' . $_board . '/' . $cite . '</a>'; @@ -2112,16 +2172,7 @@ function strip_combining_chars($str) { $o = 0; $ord = ordutf8($char, $o); - if ($ord >= 768 && $ord <= 879) - continue; - - if ($ord >= 7616 && $ord <= 7679) - continue; - - if ($ord >= 8400 && $ord <= 8447) - continue; - - if ($ord >= 65056 && $ord <= 65071) + 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)) continue; $str .= $char; @@ -2145,7 +2196,9 @@ function buildThread($id, $return = false, $mod = false) { if ($config['try_smarter'] && !$mod) $build_pages[] = thread_find_page($id); - if (!$config['smart_build'] || $return || $mod) { + $action = generation_strategy('sb_thread', array($board['uri'], $id)); + + if ($action == 'rebuild' || $return || $mod) { $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id`", $board['uri'])); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); @@ -2180,26 +2233,26 @@ function buildThread($id, $return = false, $mod = false) { )); // json api - if ($config['api']['enabled']) { + if ($config['api']['enabled'] && !$mod) { $api = new Api(); $json = json_encode($api->translateThread($thread)); $jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json'; file_write($jsonFilename, $json); } } - else { + elseif($action == 'delete') { $jsonFilename = $board['dir'] . $config['dir']['res'] . $id . '.json'; file_unlink($jsonFilename); } - if ($config['smart_build'] && !$return && !$mod) { + if ($action == 'delete' && !$return && !$mod) { $noko50fn = $board['dir'] . $config['dir']['res'] . link_for(array('id' => $id), true); file_unlink($noko50fn); file_unlink($board['dir'] . $config['dir']['res'] . link_for(array('id' => $id))); - } else if ($return) { + } elseif ($return) { return $body; - } else { + } elseif ($action == 'rebuild') { $noko50fn = $board['dir'] . $config['dir']['res'] . link_for($thread, true); if ($hasnoko50 || file_exists($noko50fn)) { buildThread50($id, $return, $mod, $thread, $antibot); @@ -2340,7 +2393,7 @@ function generate_tripcode($name) { if (isset($config['custom_tripcode']["##{$trip}"])) $trip = $config['custom_tripcode']["##{$trip}"]; else - $trip = '!!' . substr(crypt($trip, '_..A.' . substr(base64_encode(sha1($trip . $config['secure_trip_salt'], true)), 0, 4)), -10); + $trip = '!!' . substr(crypt($trip, str_replace('+', '.', '_..A.' . substr(base64_encode(sha1($trip . $config['secure_trip_salt'], true)), 0, 4))), -10); } else { if (isset($config['custom_tripcode']["#{$trip}"])) $trip = $config['custom_tripcode']["#{$trip}"]; @@ -2429,7 +2482,7 @@ function rDNS($ip_addr) { if (!$config['dns_system']) { $host = gethostbyaddr($ip_addr); } else { - $resp = shell_exec_error('host -W 1 ' . $ip_addr); + $resp = shell_exec_error('host -W 3 ' . $ip_addr); if (preg_match('/domain name pointer ([^\s]+)$/', $resp, $m)) $host = $m[1]; else @@ -2565,7 +2618,7 @@ function slugify($post) { elseif (isset ($post['body_nomarkup']) && $post['body_nomarkup']) $slug = $post['body_nomarkup']; elseif (isset ($post['body']) && $post['body']) - $slug = strip_html($post['body']); + $slug = strip_tags($post['body']); // Fix UTF-8 first $slug = mb_convert_encoding($slug, "UTF-8", "UTF-8"); @@ -2640,3 +2693,103 @@ function link_for($post, $page50 = false, $foreignlink = false, $thread = false) return sprintf($tpl, $id, $slug); } + +function prettify_textarea($s){ + return str_replace("\t", '	', str_replace("\n", ' ', htmlentities($s))); +} + +/*class HTMLPurifier_URIFilter_NoExternalImages extends HTMLPurifier_URIFilter { + public $name = 'NoExternalImages'; + public function filter(&$uri, $c, $context) { + global $config; + $ct = $context->get('CurrentToken'); + + if (!$ct || $ct->name !== 'img') return true; + + if (!isset($uri->host) && !isset($uri->scheme)) return true; + + if (!in_array($uri->scheme . '://' . $uri->host . '/', $config['allowed_offsite_urls'])) { + error('No off-site links in board announcement images.'); + } + + return true; + } +}*/ + +function purify_html($s) { + global $config; + + $c = HTMLPurifier_Config::createDefault(); + $c->set('HTML.Allowed', $config['allowed_html']); + $uri = $c->getDefinition('URI'); + $uri->addFilter(new HTMLPurifier_URIFilter_NoExternalImages(), $c); + $purifier = new HTMLPurifier($c); + $clean_html = $purifier->purify($s); + return $clean_html; +} + +function markdown($s) { + $pd = new Parsedown(); + $pd->setMarkupEscaped(true); + $pd->setimagesEnabled(false); + + return $pd->text($s); +} + +function generation_strategy($fun, $array=array()) { global $config; + $action = false; + + foreach ($config['generation_strategies'] as $s) { + if ($action = $s($fun, $array)) { + break; + } + } + + switch ($action[0]) { + case 'immediate': + return 'rebuild'; + case 'defer': + // Ok, it gets interesting here :) + get_queue('generate')->push(serialize(array('build', $fun, $array, $action))); + return 'ignore'; + case 'build_on_load': + return 'delete'; + } +} + +function strategy_immediate($fun, $array) { + return array('immediate'); +} + +function strategy_smart_build($fun, $array) { + return array('build_on_load'); +} + +function strategy_sane($fun, $array) { global $config; + if (php_sapi_name() == 'cli') return false; + else if (isset($_POST['mod'])) return false; + // Thread needs to be done instantly. Same with a board page, but only if posting a new thread. + else if ($fun == 'sb_thread' || ($fun == 'sb_board' && $array[1] == 1 && isset ($_POST['page']))) return array('immediate'); + else return false; +} + +// My first, test strategy. +function strategy_first($fun, $array) { + switch ($fun) { + case 'sb_thread': + return array('defer'); + case 'sb_board': + if ($array[1] > 8) return array('build_on_load'); + else return array('defer'); + case 'sb_api': + return array('defer'); + case 'sb_catalog': + return array('defer'); + case 'sb_recent': + return array('build_on_load'); + case 'sb_sitemap': + return array('build_on_load'); + case 'sb_ukko': + return array('defer'); + } +} diff --git a/inc/lib/Twig/Extensions/Extension/Tinyboard.php b/inc/lib/Twig/Extensions/Extension/Tinyboard.php index 028db438..3d964c71 100644 --- a/inc/lib/Twig/Extensions/Extension/Tinyboard.php +++ b/inc/lib/Twig/Extensions/Extension/Tinyboard.php @@ -17,6 +17,7 @@ class Twig_Extensions_Extension_Tinyboard extends Twig_Extension new Twig_SimpleFilter('extension', 'twig_extension_filter'), new Twig_SimpleFilter('sprintf', 'sprintf'), new Twig_SimpleFilter('capcode', 'capcode'), + new Twig_SimpleFilter('remove_modifiers', 'remove_modifiers'), new Twig_SimpleFilter('hasPermission', 'twig_hasPermission_filter'), new Twig_SimpleFilter('date', 'twig_date_filter'), new Twig_SimpleFilter('poster_id', 'poster_id'), diff --git a/inc/lock.php b/inc/lock.php new file mode 100644 index 00000000..4fb2f5df --- /dev/null +++ b/inc/lock.php @@ -0,0 +1,39 @@ +<?php +class Lock { + function __construct($key) { global $config; + if ($config['lock']['enabled'] == 'fs') { + $key = str_replace('/', '::', $key); + $key = str_replace("\0", '', $key); + + $this->f = fopen("tmp/locks/$key", "w"); + } + } + + // Get a shared lock + function get($nonblock = false) { global $config; + if ($config['lock']['enabled'] == 'fs') { + $wouldblock = false; + flock($this->f, LOCK_SH | ($nonblock ? LOCK_NB : 0), $wouldblock); + if ($nonblock && $wouldblock) return false; + } + return $this; + } + + // Get an exclusive lock + function get_ex($nonblock = false) { global $config; + if ($config['lock']['enabled'] == 'fs') { + $wouldblock = false; + flock($this->f, LOCK_EX | ($nonblock ? LOCK_NB : 0), $wouldblock); + if ($nonblock && $wouldblock) return false; + } + return $this; + } + + // Free a lock + function free() { global $config; + if ($config['lock']['enabled'] == 'fs') { + flock($this->f, LOCK_UN); + } + return $this; + } +} diff --git a/inc/mod/auth.php b/inc/mod/auth.php index 01ed5b68..42f34196 100644 --- a/inc/mod/auth.php +++ b/inc/mod/auth.php @@ -18,7 +18,18 @@ function mkhash($username, $password, $salt = false) { } // generate hash (method is not important as long as it's strong) - $hash = substr(base64_encode(md5($username . $config['cookies']['salt'] . sha1($username . $password . $salt . ($config['mod']['lock_ip'] ? $_SERVER['REMOTE_ADDR'] : ''), true), true)), 0, 20); + $hash = substr( + base64_encode( + md5( + $username . $config['cookies']['salt'] . sha1( + $username . $password . $salt . ( + $config['mod']['lock_ip'] ? $_SERVER['REMOTE_ADDR'] : '' + ), true + ) . sha1($config['password_crypt_version']) // Log out users being logged in with older password encryption schema + , true + ) + ), 0, 20 + ); if (isset($generated_salt)) return array($hash, $salt); @@ -26,25 +37,63 @@ function mkhash($username, $password, $salt = false) { return $hash; } -function generate_salt() { - mt_srand(microtime(true) * 100000 + memory_get_usage(true)); - return md5(uniqid(mt_rand(), true)); +function crypt_password_old($password) { + $salt = generate_salt(); + $password = hash('sha256', $salt . sha1($password)); + return array($salt, $password); } -function login($username, $password, $makehash=true) { - global $mod; - - // SHA1 password - if ($makehash) { - $password = sha1($password); +function crypt_password($password) { + global $config; + // `salt` database field is reused as a version value. We don't want it to be 0. + $version = $config['password_crypt_version'] ? $config['password_crypt_version'] : 1; + $new_salt = generate_salt(); + $password = crypt($password, $config['password_crypt'] . $new_salt . "$"); + return array($version, $password); +} + +function test_password($password, $salt, $test) { + global $config; + + // Version = 0 denotes an old password hashing schema. In the same column, the + // password hash was kept previously + $version = (strlen($salt) <= 8) ? (int) $salt : 0; + + if ($version == 0) { + $comp = hash('sha256', $salt . sha1($test)); + } + else { + $comp = crypt($test, $password); } + return array($version, hash_equals($password, $comp)); +} + +function generate_salt() { + // 128 bits of entropy + return strtr(base64_encode(mcrypt_create_iv(16, MCRYPT_DEV_URANDOM)), '+', '.'); +} + +function login($username, $password) { + global $mod, $config; - $query = prepare("SELECT `id`, `type`, `boards`, `password`, `salt` FROM ``mods`` WHERE `username` = :username"); + $query = prepare("SELECT `id`, `type`, `boards`, `password`, `version` FROM ``mods`` WHERE BINARY `username` = :username"); $query->bindValue(':username', $username); $query->execute() or error(db_error($query)); if ($user = $query->fetch(PDO::FETCH_ASSOC)) { - if ($user['password'] === hash('sha256', $user['salt'] . $password)) { + list($version, $ok) = test_password($user['password'], $user['version'], $password); + + if ($ok) { + if ($config['password_crypt_version'] > $version) { + // It's time to upgrade the password hashing method! + list ($user['version'], $user['password']) = crypt_password($password); + $query = prepare("UPDATE ``mods`` SET `password` = :password, `version` = :version WHERE `id` = :id"); + $query->bindValue(':password', $user['password']); + $query->bindValue(':version', $user['version']); + $query->bindValue(':id', $user['id']); + $query->execute() or error(db_error($query)); + } + return $mod = array( 'id' => $user['id'], 'type' => $user['type'], @@ -81,7 +130,7 @@ function destroyCookies() { function modLog($action, $_board=null) { global $mod, $board, $config; $query = prepare("INSERT INTO ``modlogs`` VALUES (:id, :ip, :board, :time, :text)"); - $query->bindValue(':id', $mod['id'], PDO::PARAM_INT); + $query->bindValue(':id', (isset($mod['id']) ? $mod['id'] : -1), PDO::PARAM_INT); $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); $query->bindValue(':time', time(), PDO::PARAM_INT); $query->bindValue(':text', $action); @@ -97,39 +146,6 @@ function modLog($action, $_board=null) { _syslog(LOG_INFO, '[mod/' . $mod['username'] . ']: ' . $action); } -// Validate session - -if (isset($_COOKIE[$config['cookies']['mod']])) { - // Should be username:hash:salt - $cookie = explode(':', $_COOKIE[$config['cookies']['mod']]); - if (count($cookie) != 3) { - // Malformed cookies - destroyCookies(); - mod_login(); - exit; - } - - $query = prepare("SELECT `id`, `type`, `boards`, `password` FROM ``mods`` WHERE `username` = :username"); - $query->bindValue(':username', $cookie[0]); - $query->execute() or error(db_error($query)); - $user = $query->fetch(PDO::FETCH_ASSOC); - - // validate password hash - if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) { - // Malformed cookies - destroyCookies(); - mod_login(); - exit; - } - - $mod = array( - 'id' => $user['id'], - 'type' => $user['type'], - 'username' => $cookie[0], - 'boards' => explode(',', $user['boards']) - ); -} - function create_pm_header() { global $mod, $config; @@ -163,4 +179,37 @@ function make_secure_link_token($uri) { return substr(sha1($config['cookies']['salt'] . '-' . $uri . '-' . $mod['id']), 0, 8); } - +function check_login($prompt = false) { + global $config, $mod; + // Validate session + if (isset($_COOKIE[$config['cookies']['mod']])) { + // Should be username:hash:salt + $cookie = explode(':', $_COOKIE[$config['cookies']['mod']]); + if (count($cookie) != 3) { + // Malformed cookies + destroyCookies(); + if ($prompt) mod_login(); + exit; + } + + $query = prepare("SELECT `id`, `type`, `boards`, `password` FROM ``mods`` WHERE `username` = :username"); + $query->bindValue(':username', $cookie[0]); + $query->execute() or error(db_error($query)); + $user = $query->fetch(PDO::FETCH_ASSOC); + + // validate password hash + if ($cookie[1] !== mkhash($cookie[0], $user['password'], $cookie[2])) { + // Malformed cookies + destroyCookies(); + if ($prompt) mod_login(); + exit; + } + + $mod = array( + 'id' => $user['id'], + 'type' => $user['type'], + 'username' => $cookie[0], + 'boards' => explode(',', $user['boards']) + ); + } +} diff --git a/inc/mod/pages.php b/inc/mod/pages.php index d4d8f755..491432b3 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -15,7 +15,7 @@ function mod_page($title, $template, $args, $subtitle = false) { 'hide_dashboard_link' => $template == 'mod/dashboard.html', 'title' => $title, 'subtitle' => $subtitle, - 'nojavascript' => true, + 'boardlist' => createBoardlist($mod), 'body' => Element($template, array_merge( array('config' => $config, 'mod' => $mod), @@ -612,7 +612,7 @@ function mod_news($page_no = 1) { rebuildThemes('news'); - header('Location: ?/news#' . $pdo->lastInsertId(), true, $config['redirect_http']); + header('Location: ?/edit_news#' . $pdo->lastInsertId(), true, $config['redirect_http']); } $query = prepare("SELECT * FROM ``news`` ORDER BY `id` DESC LIMIT :offset, :limit"); @@ -625,14 +625,14 @@ function mod_news($page_no = 1) { error($config['error']['404']); foreach ($news as &$entry) { - $entry['delete_token'] = make_secure_link_token('news/delete/' . $entry['id']); + $entry['delete_token'] = make_secure_link_token('edit_news/delete/' . $entry['id']); } $query = prepare("SELECT COUNT(*) FROM ``news``"); $query->execute() or error(db_error($query)); $count = $query->fetchColumn(); - mod_page(_('News'), 'mod/news.html', array('news' => $news, 'count' => $count, 'token' => make_secure_link_token('news'))); + mod_page(_('News'), 'mod/news.html', array('news' => $news, 'count' => $count, 'token' => make_secure_link_token('edit_news'))); } function mod_news_delete($id) { @@ -647,7 +647,7 @@ function mod_news_delete($id) { modLog('Deleted a news entry'); - header('Location: ?/news', true, $config['redirect_http']); + header('Location: ?/edit_news', true, $config['redirect_http']); } function mod_log($page_no = 1) { @@ -702,6 +702,42 @@ function mod_user_log($username, $page_no = 1) { mod_page(_('Moderation log'), 'mod/log.html', array('logs' => $logs, 'count' => $count, 'username' => $username)); } +function mod_board_log($board, $page_no = 1, $hide_names = false, $public = false) { + global $config; + + if ($page_no < 1) + error($config['error']['404']); + + if (!hasPermission($config['mod']['mod_board_log'], $board) && !$public) + error($config['error']['noaccess']); + + $query = prepare("SELECT `username`, `mod`, `ip`, `board`, `time`, `text` FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `board` = :board ORDER BY `time` DESC LIMIT :offset, :limit"); + $query->bindValue(':board', $board); + $query->bindValue(':limit', $config['mod']['modlog_page'], PDO::PARAM_INT); + $query->bindValue(':offset', ($page_no - 1) * $config['mod']['modlog_page'], PDO::PARAM_INT); + $query->execute() or error(db_error($query)); + $logs = $query->fetchAll(PDO::FETCH_ASSOC); + + if (empty($logs) && $page_no > 1) + error($config['error']['404']); + + if (!hasPermission($config['mod']['show_ip'])) { + // Supports ipv4 only! + foreach ($logs as $i => &$log) { + $log['text'] = preg_replace_callback('/(?:<a href="\?\/IP\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}">)?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(?:<\/a>)?/', function($matches) { + return "xxxx";//less_ip($matches[1]); + }, $log['text']); + } + } + + $query = prepare("SELECT COUNT(*) FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `board` = :board"); + $query->bindValue(':board', $board); + $query->execute() or error(db_error($query)); + $count = $query->fetchColumn(); + + mod_page(_('Board log'), 'mod/log.html', array('logs' => $logs, 'count' => $count, 'board' => $board, 'hide_names' => $hide_names, 'public' => $public)); +} + function mod_view_board($boardName, $page_no = 1) { global $config, $mod; @@ -850,7 +886,7 @@ function mod_page_ip($ip) { $args['security_token'] = make_secure_link_token('IP/' . $ip); - mod_page(sprintf('%s: %s', _('IP'), $ip), 'mod/view_ip.html', $args, $args['hostname']); + mod_page(sprintf('%s: %s', _('IP'), htmlspecialchars($ip)), 'mod/view_ip.html', $args, $args['hostname']); } function mod_ban() { @@ -1057,6 +1093,28 @@ function mod_sticky($board, $unsticky, $post) { header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']); } +function mod_cycle($board, $uncycle, $post) { + global $config; + + if (!openBoard($board)) + error($config['error']['noboard']); + + if (!hasPermission($config['mod']['cycle'], $board)) + error($config['error']['noaccess']); + + $query = prepare(sprintf('UPDATE ``posts_%s`` SET `cycle` = :cycle WHERE `id` = :id AND `thread` IS NULL', $board)); + $query->bindValue(':id', $post); + $query->bindValue(':cycle', $uncycle ? 0 : 1); + $query->execute() or error(db_error($query)); + if ($query->rowCount()) { + modLog(($uncycle ? 'Made not cyclical' : 'Made cyclical') . " thread #{$post}"); + buildThread($post); + buildIndex(); + } + + header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']); +} + function mod_bumplock($board, $unbumplock, $post) { global $config; @@ -1488,6 +1546,15 @@ function mod_edit_post($board, $edit_raw_html, $postID) { error($config['error']['404']); if (isset($_POST['name'], $_POST['email'], $_POST['subject'], $_POST['body'])) { + // Remove any modifiers they may have put in + $_POST['body'] = remove_modifiers($_POST['body']); + + // Add back modifiers in the original post + $modifiers = extract_modifiers($post['body_nomarkup']); + foreach ($modifiers as $key => $value) { + $_POST['body'] .= "<tinyboard $key>$value</tinyboard>"; + } + if ($edit_raw_html) $query = prepare(sprintf('UPDATE ``posts_%s`` SET `name` = :name, `email` = :email, `subject` = :subject, `body` = :body, `body_nomarkup` = :body_nomarkup WHERE `id` = :id', $board)); else @@ -1516,15 +1583,20 @@ function mod_edit_post($board, $edit_raw_html, $postID) { header('Location: ?/' . sprintf($config['board_path'], $board) . $config['dir']['res'] . link_for($post) . '#' . $postID, true, $config['redirect_http']); } else { + // Remove modifiers + $post['body_nomarkup'] = remove_modifiers($post['body_nomarkup']); + + $post['body_nomarkup'] = utf8tohtml($post['body_nomarkup']); + $post['body'] = utf8tohtml($post['body']); if ($config['minify_html']) { - $post['body_nomarkup'] = str_replace("\n", '
', utf8tohtml($post['body_nomarkup'])); - $post['body'] = str_replace("\n", '
', utf8tohtml($post['body'])); + $post['body_nomarkup'] = str_replace("\n", '
', $post['body_nomarkup']); + $post['body'] = str_replace("\n", '
', $post['body']); $post['body_nomarkup'] = str_replace("\r", '', $post['body_nomarkup']); $post['body'] = str_replace("\r", '', $post['body']); $post['body_nomarkup'] = str_replace("\t", '	', $post['body_nomarkup']); $post['body'] = str_replace("\t", '	', $post['body']); } - + mod_page(_('Edit post'), 'mod/edit_post_form.html', array('token' => $security_token, 'board' => $board, 'raw' => $edit_raw_html, 'post' => $post)); } } @@ -1668,6 +1740,8 @@ function mod_deletebyip($boardName, $post, $global = false) { deletePost($post['id'], false, false); rebuildThemes('post-delete', $board['uri']); + + buildIndex(); if ($post['thread']) $threads_to_rebuild[$post['board']][$post['thread']] = true; @@ -1753,13 +1827,12 @@ function mod_user($uid) { } if ($_POST['password'] != '') { - $salt = generate_salt(); - $password = hash('sha256', $salt . sha1($_POST['password'])); - - $query = prepare('UPDATE ``mods`` SET `password` = :password, `salt` = :salt WHERE `id` = :id'); + list($version, $password) = crypt_password($_POST['password']); + + $query = prepare('UPDATE ``mods`` SET `password` = :password, `version` = :version WHERE `id` = :id'); $query->bindValue(':id', $uid); $query->bindValue(':password', $password); - $query->bindValue(':salt', $salt); + $query->bindValue(':version', $version); $query->execute() or error(db_error($query)); modLog('Changed password for ' . utf8tohtml($_POST['username']) . ' <small>(#' . $user['id'] . ')</small>'); @@ -1780,13 +1853,12 @@ function mod_user($uid) { if (hasPermission($config['mod']['change_password']) && $uid == $mod['id'] && isset($_POST['password'])) { if ($_POST['password'] != '') { - $salt = generate_salt(); - $password = hash('sha256', $salt . sha1($_POST['password'])); + list($version, $password) = crypt_password($_POST['password']); - $query = prepare('UPDATE ``mods`` SET `password` = :password, `salt` = :salt WHERE `id` = :id'); + $query = prepare('UPDATE ``mods`` SET `password` = :password, `version` = :version WHERE `id` = :id'); $query->bindValue(':id', $uid); $query->bindValue(':password', $password); - $query->bindValue(':salt', $salt); + $query->bindValue(':version', $version); $query->execute() or error(db_error($query)); modLog('Changed own password'); @@ -1853,13 +1925,12 @@ function mod_user_new() { if (!isset($config['mod']['groups'][$type]) || $type == DISABLED) error(sprintf($config['error']['invalidfield'], 'type')); - $salt = generate_salt(); - $password = hash('sha256', $salt . sha1($_POST['password'])); + list($version, $password) = crypt_password($_POST['password']); - $query = prepare('INSERT INTO ``mods`` VALUES (NULL, :username, :password, :salt, :type, :boards)'); + $query = prepare('INSERT INTO ``mods`` VALUES (NULL, :username, :password, :version, :type, :boards)'); $query->bindValue(':username', $_POST['username']); $query->bindValue(':password', $password); - $query->bindValue(':salt', $salt); + $query->bindValue(':version', $version); $query->bindValue(':type', $type); $query->bindValue(':boards', implode(',', $boards)); $query->execute() or error(db_error($query)); @@ -2600,6 +2671,167 @@ function mod_theme_rebuild($theme_name) { )); } +// This needs to be done for `secure` CSRF prevention compatibility, otherwise the $board will be read in as the token if editing global pages. +function delete_page_base($page = '', $board = false) { + global $config, $mod; + + if (empty($board)) + $board = false; + + if (!$board && $mod['boards'][0] !== '*') + error($config['error']['noaccess']); + + if (!hasPermission($config['mod']['edit_pages'], $board)) + error($config['error']['noaccess']); + + if ($board !== FALSE && !openBoard($board)) + error($config['error']['noboard']); + + if ($board) { + $query = prepare('DELETE FROM ``pages`` WHERE `board` = :board AND `name` = :name'); + $query->bindValue(':board', ($board ? $board : NULL)); + } else { + $query = prepare('DELETE FROM ``pages`` WHERE `board` IS NULL AND `name` = :name'); + } + $query->bindValue(':name', $page); + $query->execute() or error(db_error($query)); + + header('Location: ?/edit_pages' . ($board ? ('/' . $board) : ''), true, $config['redirect_http']); +} + +function mod_delete_page($page = '') { + delete_page_base($page); +} + +function mod_delete_page_board($page = '', $board = false) { + delete_page_base($page, $board); +} + +function mod_edit_page($id) { + global $config, $mod, $board; + + $query = prepare('SELECT * FROM ``pages`` WHERE `id` = :id'); + $query->bindValue(':id', $id); + $query->execute() or error(db_error($query)); + $page = $query->fetch(); + + if (!$page) + error(_('Could not find the page you are trying to edit.')); + + if (!$page['board'] && $mod['boards'][0] !== '*') + error($config['error']['noaccess']); + + if (!hasPermission($config['mod']['edit_pages'], $page['board'])) + error($config['error']['noaccess']); + + if ($page['board'] && !openBoard($page['board'])) + error($config['error']['noboard']); + + if (isset($_POST['method'], $_POST['content'])) { + $content = $_POST['content']; + $method = $_POST['method']; + $page['type'] = $method; + + if (!in_array($method, array('markdown', 'html', 'infinity'))) + error(_('Unrecognized page markup method.')); + + switch ($method) { + case 'markdown': + $write = markdown($content); + break; + case 'html': + if (hasPermission($config['mod']['rawhtml'])) { + $write = $content; + } else { + $write = purify_html($content); + } + break; + case 'infinity': + $c = $content; + markup($content); + $write = $content; + $content = $c; + } + + if (!isset($write) or !$write) + error(_('Failed to mark up your input for some reason...')); + + $query = prepare('UPDATE ``pages`` SET `type` = :method, `content` = :content WHERE `id` = :id'); + $query->bindValue(':method', $method); + $query->bindValue(':content', $content); + $query->bindValue(':id', $id); + $query->execute() or error(db_error($query)); + + $fn = ($board['uri'] ? ($board['uri'] . '/') : '') . $page['name'] . '.html'; + $body = "<div class='ban'>$write</div>"; + $html = Element('page.html', array('config' => $config, 'body' => $body, 'title' => utf8tohtml($page['title']))); + file_write($fn, $html); + } + + if (!isset($content)) { + $query = prepare('SELECT `content` FROM ``pages`` WHERE `id` = :id'); + $query->bindValue(':id', $id); + $query->execute() or error(db_error($query)); + $content = $query->fetchColumn(); + } + + mod_page(sprintf(_('Editing static page: %s'), $page['name']), 'mod/edit_page.html', array('page' => $page, 'token' => make_secure_link_token("edit_page/$id"), 'content' => prettify_textarea($content), 'board' => $board)); +} + +function mod_pages($board = false) { + global $config, $mod, $pdo; + + if (empty($board)) + $board = false; + + if (!$board && $mod['boards'][0] !== '*') + error($config['error']['noaccess']); + + if (!hasPermission($config['mod']['edit_pages'], $board)) + error($config['error']['noaccess']); + + if ($board !== FALSE && !openBoard($board)) + error($config['error']['noboard']); + + if ($board) { + $query = prepare('SELECT * FROM ``pages`` WHERE `board` = :board'); + $query->bindValue(':board', $board); + } else { + $query = query('SELECT * FROM ``pages`` WHERE `board` IS NULL'); + } + $query->execute() or error(db_error($query)); + $pages = $query->fetchAll(PDO::FETCH_ASSOC); + + if (isset($_POST['page'])) { + if ($board and sizeof($pages) > $config['pages_max']) + error(sprintf(_('Sorry, this site only allows %d pages per board.'), $config['pages_max'])); + + if (!preg_match('/^[a-z0-9]{1,255}$/', $_POST['page'])) + error(_('Page names must be < 255 chars and may only contain lowercase letters A-Z and digits 1-9.')); + + foreach ($pages as $i => $p) { + if ($_POST['page'] === $p['name']) + error(_('Refusing to create a new page with the same name as an existing one.')); + } + + $title = ($_POST['title'] ? $_POST['title'] : NULL); + + $query = prepare('INSERT INTO ``pages``(board, title, name) VALUES(:board, :title, :name)'); + $query->bindValue(':board', ($board ? $board : NULL)); + $query->bindValue(':title', $title); + $query->bindValue(':name', $_POST['page']); + $query->execute() or error(db_error($query)); + + $pages[] = array('id' => $pdo->lastInsertId(), 'name' => $_POST['page'], 'board' => $board, 'title' => $title); + } + + foreach ($pages as $i => &$p) { + $p['delete_token'] = make_secure_link_token('edit_pages/delete/' . $p['name'] . ($board ? ('/' . $board) : '')); + } + + mod_page(_('Pages'), 'mod/pages.html', array('pages' => $pages, 'token' => make_secure_link_token('edit_pages' . ($board ? ('/' . $board) : '')), 'board' => $board)); +} + function mod_debug_antispam() { global $pdo, $config; @@ -2716,3 +2948,4 @@ function mod_debug_apc() { mod_page(_('Debug: APC'), 'mod/debug/apc.html', array('cached_vars' => $cached_vars)); } + diff --git a/inc/nntpchan/nntpchan.php b/inc/nntpchan/nntpchan.php new file mode 100644 index 00000000..de67a193 --- /dev/null +++ b/inc/nntpchan/nntpchan.php @@ -0,0 +1,152 @@ +<?php +/* + * Copyright (c) 2016 vichan-devel + */ + +defined('TINYBOARD') or exit; + +function gen_msgid($board, $id) { + global $config; + + $b = preg_replace("/[^0-9a-zA-Z$]/", 'x', $board); + $salt = sha1($board . "|" . $id . "|" . $config['nntpchan']['salt']); + $salt = substr($salt, 0, 7); + $salt = base_convert($salt, 16, 36); + + return "<$b.$id.$salt@".$config['nntpchan']['domain'].">"; +} + + +function gen_nntp($headers, $files) { + if (count($files) == 0) { + } + else if (count($files) == 1 && $files[0]['type'] == 'text/plain') { + $content = $files[0]['text'] . "\r\n"; + $headers['Content-Type'] = "text/plain; charset=UTF-8"; + } + else { + $boundary = sha1($headers['Message-Id']); + $content = ""; + $headers['Content-Type'] = "multipart/mixed; boundary=$boundary"; + foreach ($files as $file) { + $content .= "--$boundary\r\n"; + if (isset($file['name'])) { + $file['name'] = preg_replace('/[\r\n\0"]/', '', $file['name']); + $content .= "Content-Disposition: form-data; filename=\"$file[name]\"; name=\"attachment\"\r\n"; + } + $type = explode('/', $file['type'])[0]; + if ($type == 'text') { + $file['type'] .= '; charset=UTF-8'; + } + $content .= "Content-Type: $file[type]\r\n"; + if ($type != 'text' && $type != 'message') { + $file['text'] = base64_encode($file['text']); + $content .= "Content-Transfer-Encoding: base64\r\n"; + } + $content .= "\r\n"; + $content .= $file['text']; + $content .= "\r\n"; + } + $content .= "--$boundary--\r\n"; + + $headers['Mime-Version'] = '1.0'; + } + //$headers['Content-Length'] = strlen($content); + $headers['Date'] = date('r', $headers['Date']); + $out = ""; + foreach ($headers as $id => $val) { + $val = str_replace("\n", "\n\t", $val); + $out .= "$id: $val\r\n"; + } + $out .= "\r\n"; + $out .= $content; + return $out; +} + +function nntp_publish($msg, $id) { + global $config; + $server = $config["nntpchan"]["server"]; + $s = fsockopen("tcp://$server"); + fgets($s); + fputs($s, "MODE STREAM\r\n"); + fgets($s); + fputs($s, "TAKETHIS $id\r\n"); + fputs($s, $msg); + fputs($s, "\r\n.\r\n"); + fgets($s); + fputs($s, "QUIT\r\n"); + fclose($s); +} + +function post2nntp($post, $msgid) { + global $config; + + $headers = array(); + $files = array(); + + $headers['Message-Id'] = $msgid; + $headers['Newsgroups'] = $config['nntpchan']['group']; + $headers['Date'] = time(); + $headers['Subject'] = $post['subject'] ? $post['subject'] : "None"; + $headers['From'] = $post['name'] . " <poster@" . $config['nntpchan']['domain'] . ">"; + + if ($post['email'] == 'sage') { + $headers['X-Sage'] = true; + } + + if (!$post['op']) { + // Get muh parent + $query = prepare("SELECT `message_id` FROM ``nntp_references`` WHERE `board` = :board AND `id` = :id"); + $query->bindValue(':board', $post['board']); + $query->bindValue(':id', $post['thread']); + $query->execute() or error(db_error($query)); + + if ($result = $query->fetch(PDO::FETCH_ASSOC)) { + $headers['References'] = $result['message_id']; + } + else { + return false; // We don't have OP. Discarding. + } + } + + // Let's parse the body a bit. + $body = trim($post['body_nomarkup']); + $body = preg_replace('/\r?\n/', "\r\n", $body); + $body = preg_replace_callback('@>>(>/([a-zA-Z0-9_+-]+)/)?([0-9]+)@', function($o) use ($post) { + if ($o[1]) { + $board = $o[2]; + } + else { + $board = $post['board']; + } + $id = $o[3]; + + $query = prepare("SELECT `message_id_digest` FROM ``nntp_references`` WHERE `board` = :board AND `id` = :id"); + $query->bindValue(':board', $board); + $query->bindValue(':id', $id); + $query->execute() or error(db_error($query)); + + if ($result = $query->fetch(PDO::FETCH_ASSOC)) { + return ">>".substr($result['message_id_digest'], 0, 18); + } + else { + return $o[0]; // Should send URL imo + } + }, $body); + $body = preg_replace('/>>>>([0-9a-fA-F])+/', '>>\1', $body); + + + $files[] = array('type' => 'text/plain', 'text' => $body); + + foreach ($post['files'] as $id => $file) { + $fc = array(); + + $fc['type'] = $file['type']; + $fc['text'] = file_get_contents($file['file_path']); + $fc['name'] = $file['name']; + + $files[] = $fc; + } + + return array($headers, $files); +} diff --git a/inc/nntpchan/tests.php b/inc/nntpchan/tests.php new file mode 100644 index 00000000..a63789d7 --- /dev/null +++ b/inc/nntpchan/tests.php @@ -0,0 +1,30 @@ +<?php +define('TINYBOARD', 'fuck yeah'); +require_once('nntpchan.php'); + + +die(); + +$time = time(); +echo "\n@@@@ Thread:\n"; +echo $m0 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.0000.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None"], +[['type' => 'text/plain', 'text' => "THIS IS A NEW TEST THREAD"]]); +echo "\n@@@@ Single msg:\n"; +echo $m1 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.1234.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"], +[['type' => 'text/plain', 'text' => "hello world, with no image :("]]); +echo "\n@@@@ Single msg and pseudoimage:\n"; +echo $m2 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.2137.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"], +[['type' => 'text/plain', 'text' => "hello world, now with an image!"], + ['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif.gif"]]); +echo "\n@@@@ Single msg and two pseudoimages:\n"; +echo $m3 = gennntp(["From" => "czaks <marcin@6irc.net>", "Message-Id" => "<1234.1488.".$time."@example.vichan.net>", "Newsgroups" => "overchan.test", "Date" => time(), "Subject" => "None", "References" => "<1234.0000.".$time."@example.vichan.net>"], +[['type' => 'text/plain', 'text' => "hello world, now WITH TWO IMAGES!!!"], + ['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif.gif"], + ['type' => 'image/gif', 'text' => base64_decode("R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="), 'name' => "urgif2.gif"]]); +shoveitup($m0, "<1234.0000.".$time."@example.vichan.net>"); +sleep(1); +shoveitup($m1, "<1234.1234.".$time."@example.vichan.net>"); +sleep(1); +shoveitup($m2, "<1234.2137.".$time."@example.vichan.net>"); +shoveitup($m3, "<1234.1488.".$time."@example.vichan.net>"); + diff --git a/inc/polyfill.php b/inc/polyfill.php new file mode 100644 index 00000000..ac40a00a --- /dev/null +++ b/inc/polyfill.php @@ -0,0 +1,28 @@ +<?php + +// PHP 5.4 + +if (!function_exists('hex2bin')) { + function hex2bin($data) { + return pack("H*" , $hex_string); + } +} + +// PHP 5.6 + +if (!function_exists('hash_equals')) { + function hash_equals($ours, $theirs) { + $ours = (string)$ours; + $theirs = (string)$theirs; + + $tlen = strlen($theirs); + $olen = strlen($ours); + + $answer = 0; + for ($i = 0; $i < $tlen; $i++) { + $answer |= ord($ours[$olen > $i ? $i : 0]) ^ ord($theirs[$i]); + } + + return $answer === 0 && $olen === $tlen; + } +} diff --git a/inc/queue.php b/inc/queue.php new file mode 100644 index 00000000..66305b3b --- /dev/null +++ b/inc/queue.php @@ -0,0 +1,49 @@ +<?php + +class Queue { + function __construct($key) { global $config; + if ($config['queue']['enabled'] == 'fs') { + $this->lock = new Lock($key); + $key = str_replace('/', '::', $key); + $key = str_replace("\0", '', $key); + $this->key = "tmp/queue/$key/"; + } + } + + function push($str) { global $config; + if ($config['queue']['enabled'] == 'fs') { + $this->lock->get_ex(); + file_put_contents($this->key.microtime(true), $str); + $this->lock->free(); + } + return $this; + } + + function pop($n = 1) { global $config; + if ($config['queue']['enabled'] == 'fs') { + $this->lock->get_ex(); + $dir = opendir($this->key); + $paths = array(); + while ($n > 0) { + $path = readdir($dir); + if ($path === FALSE) break; + elseif ($path == '.' || $path == '..') continue; + else { $paths[] = $path; $n--; } + } + $out = array(); + foreach ($paths as $v) { + $out []= file_get_contents($this->key.$v); + unlink($this->key.$v); + } + $this->lock->free(); + return $out; + } + } +} + +// Don't use the constructor. Use the get_queue function. +$queues = array(); + +function get_queue($name) { global $queues; + return $queues[$name] = isset ($queues[$name]) ? $queues[$name] : new Queue($name); +} diff --git a/inc/route.php b/inc/route.php new file mode 100644 index 00000000..2a5c1732 --- /dev/null +++ b/inc/route.php @@ -0,0 +1,65 @@ +<?php + +// vichan's routing mechanism + +// don't bother with that unless you use smart build or advanced build +// you can use those parts for your own implementations though :^) + +defined('TINYBOARD') or exit; + +function route($path) { global $config; + $entrypoints = array(); + + $entrypoints['/%b/'] = 'sb_board'; + $entrypoints['/%b/'.$config['file_index']] = 'sb_board'; + $entrypoints['/%b/'.$config['file_page']] = 'sb_board'; + $entrypoints['/%b/%d.json'] = 'sb_api_board'; + if ($config['api']['enabled']) { + $entrypoints['/%b/threads.json'] = 'sb_api'; + $entrypoints['/%b/catalog.json'] = 'sb_api'; + } + + $entrypoints['/%b/'.$config['dir']['res'].$config['file_page']] = 'sb_thread_slugcheck'; + $entrypoints['/%b/'.$config['dir']['res'].$config['file_page50']] = 'sb_thread_slugcheck50'; + if ($config['slugify']) { + $entrypoints['/%b/'.$config['dir']['res'].$config['file_page_slug']] = 'sb_thread_slugcheck'; + $entrypoints['/%b/'.$config['dir']['res'].$config['file_page50_slug']] = 'sb_thread_slugcheck50'; + } + if ($config['api']['enabled']) { + $entrypoints['/%b/'.$config['dir']['res'].'%d.json'] = 'sb_thread'; + } + + $entrypoints['/*/'] = 'sb_ukko'; + $entrypoints['/*/index.html'] = 'sb_ukko'; + $entrypoints['/recent.html'] = 'sb_recent'; + $entrypoints['/%b/catalog.html'] = 'sb_catalog'; + $entrypoints['/%b/index.rss'] = 'sb_catalog'; + $entrypoints['/sitemap.xml'] = 'sb_sitemap'; + + $entrypoints = array_merge($entrypoints, $config['controller_entrypoints']); + + $reached = false; + + list($request) = explode('?', $path); + + foreach ($entrypoints as $id => $fun) { + $id = '@^' . preg_quote($id, '@') . '$@u'; + + $id = str_replace('%b', '('.$config['board_regex'].')', $id); + $id = str_replace('%d', '([0-9]+)', $id); + $id = str_replace('%s', '[a-zA-Z0-9-]+', $id); + + $matches = null; + + if (preg_match ($id, $request, $matches)) { + array_shift($matches); + + $reached = array($fun, $matches); + + break; + } + } + + return $reached; +} + |
