diff options
53 files changed, 1637 insertions, 82 deletions
@@ -1,6 +1,13 @@ Tinyboard - A lightweight PHP imageboard. ========================================== +Tinyboard + vichan-devel +------------ +Tinyboard branch taking lightweightness somewhat more liberally. Running live at +https://pl.vichan.net/ (Polish) and http://vichan.net/ (International; may be outdated). + +It contains many changes from original Tinyboard, mainly in frontend area. + About ------------ Tinyboard is a light-weight, fast, highly configurable and user-friendly diff --git a/attentionbar.php b/attentionbar.php new file mode 100644 index 00000000..e928daae --- /dev/null +++ b/attentionbar.php @@ -0,0 +1,10 @@ +<?php + require_once 'inc/functions.php'; + checkBan(); + if(@strlen($_POST["text"])>0) { + file_put_contents("attentionbar.txt",$_POST["text"]); + if(strlen($_SERVER['HTTP_REFERER'])>0) { header('Location: ' . $_SERVER['HTTP_REFERER']); } + else { header('Location: /'); } + } else print(file_get_contents("attentionbar.txt")); + return; +?> diff --git a/attentionbar.txt b/attentionbar.txt new file mode 100644 index 00000000..50aa355b --- /dev/null +++ b/attentionbar.txt @@ -0,0 +1 @@ +- * ( Pasek Atencji ) * - diff --git a/banned.php b/banned.php new file mode 100644 index 00000000..57e4a9bc --- /dev/null +++ b/banned.php @@ -0,0 +1,7 @@ +<?php + require_once 'inc/functions.php'; + checkBan(); + print "<!doctype html><html><head><meta charset='utf-8'><title>"._("Banned?")."</title></head><body>"; + print "<h1>"._("You are not banned.")."</h1>"; + print "</body></html>"; +?> diff --git a/inc/api.php b/inc/api.php new file mode 100644 index 00000000..5592d8af --- /dev/null +++ b/inc/api.php @@ -0,0 +1,123 @@ +<?php + +/* + * Copyright (c) 2010-2013 Tinyboard Development Group + */ + +/** + * Class for generating json API compatible with 4chan API + */ +class Api { + + /** + * Translation from local fields to fields in 4chan-style API + */ + public static $postFields = array( + 'id' => 'no', + 'thread' => 'resto', + 'subject' => 'sub', + 'email' => 'email', + 'name' => 'name', + 'trip' => 'trip', + 'capcode' => 'capcode', + 'body' => 'com', + 'time' => 'time', + 'thumb' => 'thumb', // non-compatible field + 'thumbx' => 'tn_w', + 'thumby' => 'tn_h', + 'file' => 'file', // non-compatible field + 'filex' => 'w', + 'filey' => 'h', + 'filesize' => 'fsize', + //'filename' => 'filename', + 'omitted' => 'omitted_posts', + 'omitted_images' => 'omitted_images', + //'posts' => 'replies', + //'ip' => '', + 'sticky' => 'sticky', + 'locked' => 'locked', + //'bumplocked' => '', + //'embed' => '', + //'root' => '', + //'mod' => '', + //'hr' => '', + ); + + static $ints = array( + 'no' => 1, + 'resto' => 1, + 'time' => 1, + 'tn_w' => 1, + 'tn_h' => 1, + 'w' => 1, + 'h' => 1, + 'fsize' => 1, + 'omitted_posts' => 1, + 'omitted_images' => 1, + 'sticky' => 1, + 'locked' => 1, + ); + + private function translatePost($post) { + $apiPost = array(); + foreach (self::$postFields as $local => $translated) { + if (!isset($post->$local)) + continue; + + $toInt = isset(self::$ints[$translated]); + $val = $post->$local; + if ($val !== null && $val !== '') { + $apiPost[$translated] = $toInt ? (int) $val : $val; + } + } + + if (isset($post->filename)) { + $dotPos = strrpos($post->filename, '.'); + $apiPost['filename'] = substr($post->filename, 0, $dotPos); + $apiPost['ext'] = substr($post->filename, $dotPos); + } + + return $apiPost; + } + + function translateThread(Thread $thread) { + $apiPosts = array(); + $op = $this->translatePost($thread); + $op['resto'] = 0; + $apiPosts['posts'][] = $op; + + foreach ($thread->posts as $p) { + $apiPosts['posts'][] = $this->translatePost($p); + } + + return $apiPosts; + } + + function translatePage(array $threads) { + $apiPage = array(); + foreach ($threads as $thread) { + $apiPage['threads'][] = $this->translateThread($thread); + } + return $apiPage; + } + + function translateCatalogPage(array $threads) { + $apiPage = array(); + foreach ($threads as $thread) { + $ts = $this->translateThread($thread); + $apiPage['threads'][] = current($ts['posts']); + } + return $apiPage; + } + + function translateCatalog($catalog) { + $apiCatalog = array(); + foreach ($catalog as $page => $threads) { + $apiPage = $this->translateCatalogPage($threads); + $apiPage['page'] = $page; + $apiCatalog[] = $apiPage; + } + + return $apiCatalog; + } +} diff --git a/inc/config.php b/inc/config.php index 06c6c243..d9210ab5 100644 --- a/inc/config.php +++ b/inc/config.php @@ -345,9 +345,10 @@ $config['auto_unicode'] = true; // Whether to turn URLs into functional links. $config['markup_urls'] = true; + // Optional URL prefix for links (eg. "http://anonym.to/?"). $config['link_prefix'] = ''; - + $config['url_ads'] = &$config['link_prefix']; // leave alias // 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. @@ -615,6 +616,10 @@ // Number of reports you can create at once. $config['report_limit'] = 3; + // Attention Whoring Bar + // REMEMBER TO CHMOD attentionbar.txt PROPERLY + // Oh, and add jQuery in additional_javascript. + $config['attention_bar'] = false; /* * ==================== * Display settings @@ -1192,6 +1197,28 @@ /* * ==================== + * Public post search + * ==================== + */ + $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); + + // Global maximal number of queries per minutes + $config['search']['queries_per_minutes_all'] = Array(50, 2); + + // Limit of search results + $config['search']['search_limit'] = 100; + + // Boards for searching + //$config['search']['boards'] = array('a', 'b', 'c', 'd', 'e'); + +/* + * ==================== * Events (PHP 5.3.0+) * ==================== */ diff --git a/inc/display.php b/inc/display.php index 97b8c744..18eeb1f6 100644 --- a/inc/display.php +++ b/inc/display.php @@ -25,8 +25,8 @@ function doBoardListPart($list, $root) { $body = ''; foreach ($list as $board) { if (is_array($board)) - $body .= ' [' . doBoardListPart($board, $root) . '] '; - // $body .= ' <span class="sub">[' . doBoardListPart($board, $root) . ']</span> '; + // $body .= ' [' . doBoardListPart($board, $root) . '] '; + $body .= ' <span class="sub">[' . doBoardListPart($board, $root) . ']</span> '; else { if (($key = array_search($board, $list)) && gettype($key) == 'string') { $body .= ' <a href="' . $board . '">' . $key . '</a> /'; diff --git a/inc/functions.php b/inc/functions.php index 8b2c4c7b..4c3450f3 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -13,6 +13,7 @@ require_once 'inc/display.php'; require_once 'inc/template.php'; require_once 'inc/database.php'; require_once 'inc/events.php'; +require_once 'inc/api.php'; require_once 'inc/lib/gettext/gettext.inc'; // the user is not currently logged in as a moderator @@ -1030,6 +1031,7 @@ function index($page, $mod=false) { if ($query->rowCount() < 1 && $page > 1) return false; + while ($th = $query->fetch(PDO::FETCH_ASSOC)) { $thread = new Thread( $th['id'], $th['subject'], $th['email'], $th['name'], $th['trip'], $th['capcode'], $th['body'], $th['time'], $th['thumb'], @@ -1079,6 +1081,7 @@ function index($page, $mod=false) { $thread->omitted_images = $omitted['image_count'] - $num_images; } + $threads[] = $thread; $body .= $thread->build(true); } @@ -1087,7 +1090,8 @@ function index($page, $mod=false) { 'body' => $body, 'post_url' => $config['post_url'], 'config' => $config, - 'boardlist' => createBoardlist($mod) + 'boardlist' => createBoardlist($mod), + 'threads' => $threads ); } @@ -1280,6 +1284,9 @@ function buildIndex() { if (!$config['try_smarter']) $antibot = create_antibot($board['uri']); + $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)); @@ -1300,13 +1307,31 @@ function buildIndex() { $content['antibot'] = $antibot; file_write($filename, Element('index.html', $content)); + + // json api + $threads = $content['threads']; + $json = json_encode($api->translatePage($threads)); + $jsonFilename = $board['dir'] . ($page-1) . ".json"; // pages should start from 0 + file_write($jsonFilename, $json); + + $catalog[$page-1] = $threads; + + $page++; } if ($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); + + $jsonFilename = $board['dir'] . ($page-1) . ".json"; + file_unlink($jsonFilename); } } + + // json api catalog + $json = json_encode($api->translateCatalog($catalog)); + $jsonFilename = $board['dir'] . "catalog.json"; + file_write($jsonFilename, $json); } function buildJavascript() { @@ -1751,6 +1776,12 @@ function buildThread($id, $return = false, $mod = false) { return $body; file_write($board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $id), $body); + + // json api + $api = new Api(); + $json = json_encode($api->translateThread($thread)); + $jsonFilename = $board['dir'] . $config['dir']['res'] . $id . ".json"; + file_write($jsonFilename, $json); } function rrmdir($dir) { diff --git a/install.php b/install.php index 81b2e136..4b3785e0 100644 --- a/install.php +++ b/install.php @@ -1,7 +1,7 @@ <?php // Installation/upgrade file -define('VERSION', 'v0.9.6-dev-11'); +define('VERSION', 'v0.9.6-dev-11 + <a href="https://int.vichan.net/devel/">vichan-devel-4.0.6</a>'); require 'inc/functions.php'; @@ -228,8 +228,12 @@ if (file_exists($config['has_installed'])) { query(sprintf("ALTER TABLE `posts_%s` DROP INDEX `thread`", $_board['uri'])) or error(db_error()); } case 'v0.9.6-dev-7': + case 'v0.9.6-dev-7 + <a href="https://github.com/vichan-devel/Tinyboard/">vichan-devel-4.0-gold</a>': query("ALTER TABLE `bans` ADD `seen` BOOLEAN NOT NULL") or error(db_error()); case 'v0.9.6-dev-8': + case 'v0.9.6-dev-8 + <a href="https://github.com/vichan-devel/Tinyboard/">vichan-devel-4.0.1</a>': + query("CREATE TABLE IF NOT EXISTS `search_queries` ( `ip` varchar(39) NOT NULL, `time` int(11) NOT NULL, `query` text NOT NULL) ENGINE=MyISAM DEFAULT CHARSET=utf8;") or error(db_error()); + case 'v0.9.6-dev-8 + <a href="https://github.com/vichan-devel/Tinyboard/">vichan-devel-4.0.2</a>': query("ALTER TABLE `mods` CHANGE `password` `password` CHAR( 64 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'SHA256'") or error(db_error()); query("ALTER TABLE `mods` ADD `salt` CHAR( 32 ) NOT NULL AFTER `password`") or error(db_error()); $query = query("SELECT `id`,`password` FROM `mods`") or error(db_error()); @@ -248,7 +252,10 @@ if (file_exists($config['has_installed'])) { $_query->execute() or error(db_error($_query)); } } - case 'v0.9.6-dev-9': + case 'v0.9.6-dev-9': + case 'v0.9.6-dev-9 + <a href="https://github.com/vichan-devel/Tinyboard/">vichan-devel-4.0.3</a>': + case 'v0.9.6-dev-9 + <a href="https://github.com/vichan-devel/Tinyboard/">vichan-devel-4.0.4-gold</a>': + case 'v0.9.6-dev-9 + <a href="https://github.com/vichan-devel/Tinyboard/">vichan-devel-4.0.5-gold</a>': sql_open(); function __query($sql) { if (mysql_version() >= 50503) @@ -356,6 +363,7 @@ if (file_exists($config['has_installed'])) { CHANGE `boards` `boards` TEXT CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL;") or error(db_error()); query("ALTER TABLE `reports` CHANGE `board` `board` VARCHAR( 58 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL;") or error(db_error()); + case 'v0.9.6-dev-11': case false: // Update version number file_write($config['has_installed'], VERSION); diff --git a/install.sql b/install.sql index db89452b..84090999 100644 --- a/install.sql +++ b/install.sql @@ -230,6 +230,18 @@ CREATE TABLE IF NOT EXISTS `robot` ( -- -------------------------------------------------------- -- +-- Table structure for table `search_queries` +-- + +CREATE TABLE IF NOT EXISTS `search_queries` ( + `ip` varchar(39) NOT NULL, + `time` int(11) NOT NULL, + `query` text NOT NULL +) ENGINE=MyISAM DEFAULT CHARSET=utf8; + +-- -------------------------------------------------------- + +-- -- Table structure for table `theme_settings` -- diff --git a/js/attention-bar.js b/js/attention-bar.js new file mode 100644 index 00000000..a84cadff --- /dev/null +++ b/js/attention-bar.js @@ -0,0 +1,8 @@ +$(document).ready(function(){ + $("#attention_bar").click(function(eO){ $("#attention_bar").css("display","none"); + $("#attention_bar_form").css("display","block"); }); + $.get(configRoot + "attentionbar.txt", function(data) { + $("#attention_bar").text(data); + $("#attention_bar_input").val(data); + }); +}); diff --git a/js/chrome-board-expanding-fix.js b/js/chrome-board-expanding-fix.js new file mode 100644 index 00000000..f0a5930a --- /dev/null +++ b/js/chrome-board-expanding-fix.js @@ -0,0 +1,7 @@ +$(document).ready(function(){ + $(".desktop-style .sub .sub").on("mouseover", function() { + $(this).addClass("hover"); + }).on("mouseout", function() { + $(this).removeClass("hover"); + }); +}); diff --git a/js/expand.js b/js/expand.js index 07ce9e43..b13d0bcf 100644 --- a/js/expand.js +++ b/js/expand.js @@ -14,8 +14,7 @@ $(document).ready(function(){ if($('div.banner').length != 0) return; // not index - $('div.post.op span.omitted').each(function() { - + var do_expand = function() { $(this) .html($(this).text().replace(_("Click reply to view."), '<a href="javascript:void(0)">'+_("Click to expand")+'</a>.')) diff --git a/js/hide-threads.js b/js/hide-threads.js index 575804b6..44bd829b 100644 --- a/js/hide-threads.js +++ b/js/hide-threads.js @@ -12,11 +12,9 @@ */ $(document).ready(function(){ - if($('div.banner').length != 0) + if (active_page != "index" && active_page != "ukko") return; // not index - - var board = $('form input[name="board"]').val().toString(); - + if (!localStorage.hiddenthreads) localStorage.hiddenthreads = '{}'; @@ -37,13 +35,16 @@ $(document).ready(function(){ } } - if (!hidden_data[board]) { - hidden_data[board] = {}; // id : timestamp - } - var do_hide_threads = function() { var id = $(this).children('p.intro').children('a.post_no:eq(1)').text(); var thread_container = $(this).parent(); + + var board = thread_container.data("board"); + + if (!hidden_data[board]) { + hidden_data[board] = {}; // id : timestamp + } + $('<a class="hide-thread-link" style="float:left;margin-right:5px" href="javascript:void(0)">[–]</a><span> </span>') .insertBefore(thread_container.find(':not(h2,h2 *):first')) .click(function() { diff --git a/js/inline-expanding-filename.js b/js/inline-expanding-filename.js new file mode 100644 index 00000000..048a0e1a --- /dev/null +++ b/js/inline-expanding-filename.js @@ -0,0 +1,33 @@ +/* + * inline-expanding-filename.js + * Binds image filename link to expanding, to make kusaba x users somewhat more accustomed. + * https://github.com/vichan-devel/Tinyboard/blob/master/js/inline-expanding.js + * + * Released under the MIT license + * Copyright (c) 2012-2013 Marcin Łabanowski <marcin@6irc.net> + * + * Usage: + * $config['additional_javascript'][] = 'js/jquery.min.js'; + * $config['additional_javascript'][] = 'js/inline-expanding.js'; + * $config['additional_javascript'][] = 'js/inline-expanding-filename.js'; + * + */ + +onready(function(){ + var inline_expanding_filename = function() { + $(this).find(".fileinfo > a").click(function(){ + var imagelink = $(this).parent().parent().find('a[target="_blank"]:first'); + if(imagelink.length > 0) { + imagelink.click(); + return false; + } + }); + }; + + $('div[id^="thread_"]').each(inline_expanding_filename); + + // allow to work with auto-reload.js, etc. + $(document).bind('new_post', function(e, post) { + inline_expanding_filename.call(post); + }); +}); diff --git a/js/inline-expanding.js b/js/inline-expanding.js index 61085b0d..398db181 100644 --- a/js/inline-expanding.js +++ b/js/inline-expanding.js @@ -65,5 +65,4 @@ onready(function(){ } else { inline_expand_post.call(document); } - }); diff --git a/js/mobile-style.js b/js/mobile-style.js new file mode 100644 index 00000000..89e01b73 --- /dev/null +++ b/js/mobile-style.js @@ -0,0 +1,8 @@ +if(navigator.userAgent.match(/iPhone|iPod|iPad|Android|Opera Mini|Blackberry|PlayBook|Windows Phone|Tablet PC|Windows CE|IEMobile/i)) { + $('html').addClass("mobile-style"); + device_type = "mobile"; +} +else { + $('html').addClass("desktop-style"); + device_type = "desktop"; +} diff --git a/js/post-hider.js b/js/post-hider.js new file mode 120000 index 00000000..693c14d6 --- /dev/null +++ b/js/post-hider.js @@ -0,0 +1 @@ +hide-threads.js
\ No newline at end of file diff --git a/js/quick-reply.js b/js/quick-reply.js index ea504b48..bee204b7 100644 --- a/js/quick-reply.js +++ b/js/quick-reply.js @@ -12,6 +12,7 @@ * */ +if (active_page == 'index') { $(document).ready(function(){ if($('div.banner').length != 0) return; // not index @@ -43,4 +44,4 @@ $(document).ready(function(){ }); }); }); - +} diff --git a/js/smartphone-spoiler.js b/js/smartphone-spoiler.js index 54c7a9a1..ee904b54 100644 --- a/js/smartphone-spoiler.js +++ b/js/smartphone-spoiler.js @@ -6,18 +6,28 @@ * Copyright (c) 2012 Michael Save <savetheinternet@tinyboard.org> * * Usage: + * $config['additional_javascript'][] = 'js/mobile-style.js'; * $config['additional_javascript'][] = 'js/smartphone-spoiler.js'; * */ onready(function(){ - if(navigator.userAgent.match(/iPhone|iPod|iPad|Android|Opera Mini|Blackberry|PlayBook|Windows Phone|Tablet PC|Windows CE|IEMobile/i)) { - var spoilers = document.getElementsByClassName('spoiler'); - for(var i = 0; i < spoilers.length; i++) { - spoilers[i].onmousedown = function() { - this.style.color = 'white'; - }; - } + if(device_type == 'mobile') { + var fix_spoilers = function(where) { + var spoilers = where.getElementsByClassName('spoiler'); + for(var i = 0; i < spoilers.length; i++) { + spoilers[i].onmousedown = function() { + this.style.color = 'white'; + }; + } + }; + fix_spoilers(document); + + // allow to work with auto-reload.js, etc. + $(document).bind('new_post', function(e, post) { + fix_spoilers(post); + }); + } }); @@ -79,6 +79,7 @@ if (isset($_POST['delete'])) { $is_mod = isset($_POST['mod']) && $_POST['mod']; $root = $is_mod ? $config['root'] . $config['file_mod'] . '?/' : $config['root']; + if (!$is_mod) header('X-Associated-Content: "' . $root . $board['dir'] . $config['file_index'] . '"'); header('Location: ' . $root . $board['dir'] . $config['file_index'], true, $config['redirect_http']); } elseif (isset($_POST['report'])) { @@ -136,6 +137,7 @@ if (isset($_POST['delete'])) { $is_mod = isset($_POST['mod']) && $_POST['mod']; $root = $is_mod ? $config['root'] . $config['file_mod'] . '?/' : $config['root']; + if (!$is_mod) header('X-Associated-Content: "' . $root . $board['dir'] . $config['file_index'] . '"'); header('Location: ' . $root . $board['dir'] . $config['file_index'], true, $config['redirect_http']); } elseif (isset($_POST['post'])) { @@ -665,6 +667,8 @@ if (isset($_POST['delete'])) { _syslog(LOG_INFO, 'New post: /' . $board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $post['op'] ? $id : $post['thread']) . (!$post['op'] ? '#' . $id : '')); + if (!$post['mod']) header('X-Associated-Content: "' . $redirect . '"'); + if ($post['op']) rebuildThemes('post-thread', $board['uri']); else diff --git a/search.php b/search.php new file mode 100644 index 00000000..a40c4701 --- /dev/null +++ b/search.php @@ -0,0 +1,170 @@ +<?php + require 'inc/functions.php'; + + if (!$config['search']['enable']) { + die(_("Post search is disabled")); + } + + $queries_per_minutes = $config['search']['queries_per_minutes']; + $queries_per_minutes_all = $config['search']['queries_per_minutes_all']; + $search_limit = $config['search']['search_limit']; + + $boards = $config['search']['boards']; + + $body = Element('search_form.html', Array('boards' => $boards, 'board' => isset($_GET['board']) ? $_GET['board'] : false, 'search' => isset($_GET['search']) ? str_replace('"', '"', utf8tohtml($_GET['search'])) : false)); + + if(isset($_GET['search']) && !empty($_GET['search']) && isset($_GET['board']) && in_array($_GET['board'], $boards)) { + $phrase = $_GET['search']; + $_body = ''; + + $query = prepare("SELECT COUNT(*) FROM ``search_queries`` WHERE `ip` = :ip AND `time` > :time"); + $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); + $query->bindValue(':time', time() - ($queries_per_minutes[1] * 60)); + $query->execute() or error(db_error($query)); + if($query->fetchColumn() > $queries_per_minutes[0]) + error(_('Wait a while before searching again, please.')); + + $query = prepare("SELECT COUNT(*) FROM ``search_queries`` WHERE `time` > :time"); + $query->bindValue(':time', time() - ($queries_per_minutes_all[1] * 60)); + $query->execute() or error(db_error($query)); + if($query->fetchColumn() > $queries_per_minutes_all[0]) + error(_('Wait a while before searching again, please.')); + + + $query = prepare("INSERT INTO ``search_queries`` VALUES (:ip, :time, :query)"); + $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); + $query->bindValue(':time', time()); + $query->bindValue(':query', $phrase); + $query->execute() or error(db_error($query)); + + _syslog(LOG_NOTICE, 'Searched /' . $_GET['board'] . '/ for "' . $phrase . '"'); + + // Cleanup search queries table + $query = prepare("DELETE FROM ``search_queries`` WHERE `time` <= :time"); + $query->bindValue(':time', time() - ($queries_per_minutes_all[1] * 60)); + $query->execute() or error(db_error($query)); + + openBoard($_GET['board']); + + $filters = Array(); + + function search_filters($m) { + global $filters; + $name = $m[2]; + $value = isset($m[4]) ? $m[4] : $m[3]; + + if(!in_array($name, array('id', 'thread', 'subject', 'name'))) { + // unknown filter + return $m[0]; + } + + $filters[$name] = $value; + + return $m[1]; + } + + $phrase = trim(preg_replace_callback('/(^|\s)(\w+):("(.*)?"|[^\s]*)/', 'search_filters', $phrase)); + + if(!preg_match('/[^*^\s]/', $phrase) && empty($filters)) { + _syslog(LOG_WARNING, 'Query too broad.'); + $body .= '<p class="unimportant" style="text-align:center">(Query too broad.)</p>'; + echo Element('page.html', Array( + 'config'=>$config, + 'title'=>'Search', + 'body'=>$body, + )); + exit; + } + + // Escape escape character + $phrase = str_replace('!', '!!', $phrase); + + // Remove SQL wildcard + $phrase = str_replace('%', '!%', $phrase); + + // Use asterisk as wildcard to suit convention + $phrase = str_replace('*', '%', $phrase); + + // Remove `, it's used by table prefix magic + $phrase = str_replace('`', '!`', $phrase); + + $like = ''; + $match = Array(); + + // Find exact phrases + if(preg_match_all('/"(.+?)"/', $phrase, $m)) { + foreach($m[1] as &$quote) { + $phrase = str_replace("\"{$quote}\"", '', $phrase); + $match[] = $pdo->quote($quote); + } + } + + $words = explode(' ', $phrase); + foreach($words as &$word) { + if(empty($word)) + continue; + $match[] = $pdo->quote($word); + } + + $like = ''; + foreach($match as &$phrase) { + if(!empty($like)) + $like .= ' AND '; + $phrase = preg_replace('/^\'(.+)\'$/', '\'%$1%\'', $phrase); + $like .= '`body` LIKE ' . $phrase . ' ESCAPE \'!\''; + } + + foreach($filters as $name => $value) { + if(!empty($like)) + $like .= ' AND '; + $like .= '`' . $name . '` = '. $pdo->quote($value); + } + + $like = str_replace('%', '%%', $like); + + $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE " . $like . " ORDER BY `time` DESC LIMIT :limit", $board['uri'])); + $query->bindValue(':limit', $search_limit, PDO::PARAM_INT); + $query->execute() or error(db_error($query)); + + if($query->rowCount() == $search_limit) { + _syslog(LOG_WARNING, 'Query too broad.'); + $body .= '<p class="unimportant" style="text-align:center">('._('Query too broad.').')</p>'; + echo Element('page.html', Array( + 'config'=>$config, + 'title'=>'Search', + 'body'=>$body, + )); + exit; + } + + $temp = ''; + while($post = $query->fetch()) { + if(!$post['thread']) { + $po = new Thread($post['id'], $post['subject'], $post['email'], $post['name'], $post['trip'], $post['capcode'], $post['body'], $post['time'], $post['thumb'], $post['thumbwidth'], $post['thumbheight'], $post['file'], $post['filewidth'], $post['fileheight'], $post['filesize'], $post['filename'], $post['ip'], $post['sticky'], $post['locked'], $post['sage'], $post['embed']); + } else { + $po = new Post($post['id'], $post['thread'], $post['subject'], $post['email'], $post['name'], $post['trip'], $post['capcode'], $post['body'], $post['time'], $post['thumb'], $post['thumbwidth'], $post['thumbheight'], $post['file'], $post['filewidth'], $post['fileheight'], $post['filesize'], $post['filename'], $post['ip'], $post['embed']); + } + $temp .= $po->build(true) . '<hr/>'; + } + + if(!empty($temp)) + $_body .= '<fieldset><legend>' . + sprintf(ngettext('%d result in', '%d results in', $query->rowCount()), + $query->rowCount()) . ' <a href="/' . + sprintf($config['board_path'], $board['uri']) . $config['file_index'] . + '">' . + sprintf($config['board_abbreviation'], $board['uri']) . ' - ' . $board['title'] . + '</a></legend>' . $temp . '</fieldset>'; + + $body .= '<hr/>'; + if(!empty($_body)) + $body .= $_body; + else + $body .= '<p style="text-align:center" class="unimportant">('._('No results.').')</p>'; + } + + echo Element('page.html', Array( + 'config'=>$config, + 'title'=>_('Search'), + 'body'=>'' . $body + )); diff --git a/stylesheets/dark.css b/stylesheets/dark.css index c4a6855c..0982c278 100644 --- a/stylesheets/dark.css +++ b/stylesheets/dark.css @@ -9,7 +9,7 @@ body { font-size: 12px; } span.quote { - color:#B8D962; + color:#B8D962; } h1 { font-size: 20pt; @@ -45,10 +45,10 @@ a.post_no:hover { div.post.reply { background: #333333; border: #555555 1px solid; - -webkit-border-radius: 10px; - -khtml-border-radius: 10px; - -moz-border-radius: 10px; - border-radius: 10px; + -webkit-border-radius: 10px; + -khtml-border-radius: 10px; + -moz-border-radius: 10px; + border-radius: 10px; } div.post.reply.highlighted { @@ -112,7 +112,7 @@ div.banner { font-size: 12px; } div.banner a { - color:#000; + color:#000; } input[type="submit"] { background: #333333; @@ -125,7 +125,7 @@ input[type="submit"]:hover { color: #32DD72; } input[type="text"]:focus { - border:#aaa 1px solid; + border:#aaa 1px solid; } p.fileinfo a:hover { text-decoration: underline; @@ -149,7 +149,7 @@ hr { } div.boardlist { color: #999999; - background-color: rgba(12%, 12%, 12%, 0.10); + background-color: rgba(12%, 12%, 12%, 0.10); } div.ban { @@ -173,4 +173,4 @@ table.modlog tr th { .desktop-style div.boardlist:nth-child(1):hover { background-color: rgba(30%, 30%, 30%, 0.65); -}
\ No newline at end of file +} diff --git a/stylesheets/futaba+vichan.css b/stylesheets/futaba+vichan.css index be0ab672..adf9baef 100644 --- a/stylesheets/futaba+vichan.css +++ b/stylesheets/futaba+vichan.css @@ -86,3 +86,19 @@ unimportant, .unimportant * { table.modlog tr th { background: #EA8; } +div.boardlist { + background-color: rgba(90%,90%,90%,0.2); +} +div.boardlist .sub { + background: none; +} +div.boardlist.bottom { + background-color:transparent; +} + +.desktop-style div.boardlist:nth-child(1) { + text-shadow: #fff 1px 1px 1px, #fff -1px -1px 1px; +} +.desktop-style div.boardlist:nth-child(1):hover { + background-color: rgba(90%, 90%, 90%, 0.55); +} diff --git a/stylesheets/futaba.css b/stylesheets/futaba.css index d30cb938..2a30bc00 100644 --- a/stylesheets/futaba.css +++ b/stylesheets/futaba.css @@ -75,3 +75,12 @@ unimportant, .unimportant * { table.modlog tr th { background: #EA8; } + +.desktop-style div.boardlist:nth-child(1) { + text-shadow: #fff 1px 1px 1px, #fff -1px -1px 1px; +} + + +.desktop-style div.boardlist:nth-child(1):hover { + background-color: rgba(90%, 90%, 90%, 0.55); +} diff --git a/stylesheets/img/arrow.png b/stylesheets/img/arrow.png Binary files differnew file mode 100644 index 00000000..f7403695 --- /dev/null +++ b/stylesheets/img/arrow.png diff --git a/stylesheets/miku.css b/stylesheets/miku.css index 23fb1d9d..2f55b48c 100644 --- a/stylesheets/miku.css +++ b/stylesheets/miku.css @@ -8,7 +8,7 @@ body { a:link, a:visited { text-decoration: none; - color: #0093AB; + color: #00637B; } a:link:hover, a:visited:hover { @@ -54,7 +54,7 @@ p.intro span.name { } div.post.reply.highlighted { - background: #9988EE; + background: #a9d8ff; } div.post.reply { @@ -84,4 +84,14 @@ hr { div.boardlist { color: #0093AB; + background-color: rgba(65%, 85%, 95%, 0.2); +} + +.desktop-style div.boardlist:nth-child(1) { + text-shadow: #D2FFEE 1px 1px 1px, #D2FFEE -1px -1px 1px; +} + + +.desktop-style div.boardlist:nth-child(1):hover { + background-color: rgba(70%, 95%, 100%, 0.45); } diff --git a/stylesheets/style.css b/stylesheets/style.css index c0430b7c..fbb5370f 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -285,6 +285,7 @@ span.spoiler:hover, div.post.reply div.body span.spoiler:hover a { div.styles { float: right; padding-bottom: 20px; + } div.styles a { margin: 0 10px; @@ -404,8 +405,59 @@ table.mod.config-editor td { table.mod.config-editor input[type="text"] { width: 98%; } +.desktop-style div.boardlist:nth-child(1) { + position: fixed; + top: 0px; + left: 0px; + right: 0px; + /*text-shadow: white 1px 0px 6px, white 1px 0px 6px, black 1px 0px 3px;*/ + margin-top: 0px; + z-index: 30; +} +.desktop-style div.boardlist:nth-child(1):hover { + background-color: rgba(90%, 90%, 90%, 0.6); +} + +.desktop-style body { + padding-top: 20px; +} + +.desktop-style .sub { + background: inherit; +} +.desktop-style .sub .sub { + display: inline-block; + text-indent: -9000px; + width: 7px; + background: url('img/arrow.png') right center no-repeat; +} +.desktop-style .sub .sub:hover, .desktop-style .sub .sub.hover { + display: inline; + text-indent: 0px; + background: inherit; +} + +#attention_bar { + height: 1.5em; + width: 100%; + text-align: center; +} +#attention_bar_form { + display: none; + padding: 0; + margin: 0; +} +#attention_bar_input { + width: 100%; + padding: 0; + margin: 0; + text-align: center; +} +#attention_bar:hover { + background-color: #fff; + opacity: 0.8; +} p.intro.thread-hidden { margin: 0px; padding: 0px; } - diff --git a/stylesheets/testorange.css b/stylesheets/testorange.css index 7b1ce0a6..c7250de4 100644 --- a/stylesheets/testorange.css +++ b/stylesheets/testorange.css @@ -12,6 +12,19 @@ body { background-position: right bottom, 0% 0%, 0% 0%; } +.desktop-style .bl-menu{ +background-image: url('img/testorange_bg.gif'); + background-repeat: repeat; +} + +.boardlist .board a { +background-image: url('img/testorange_bg.gif'); + background-repeat: repeat; +} + + + + a:link, a:visited, div.post.reply div.body a { color:#ff9100; text-decoration: none; @@ -22,7 +35,10 @@ a:link:hover, a:visited:hover, div.post.reply div.body a:hover { a.email span.name { color:#ff9100 !important; +} +div.post.reply.highlighted { +background: black; } input { @@ -150,11 +166,10 @@ span.trip, .postertrip { .omittedposts { color:#707070; } -.reply { +div.post.reply { padding: 5px; border: 1px #707070 solid !important; - background: #282828 !important; - + background: #282828; -webkit-border-radius: 10px; -khtml-border-radius: 10px; -moz-border-radius: 10px; @@ -164,45 +179,33 @@ span.trip, .postertrip { background-color: #1e1e1e; color: #bebebe; } -.doubledash { - vertical-align:top; - clear:both; - float:left; -} -.replytitle { - font-size: 1.2em; - color:#fff; - font-weight:800; -} -.commentpostername { - color: #fff; - font-weight:800; -} -.thumbnailmsg { - font-size: small; - color: #bebebe; -} form table th, div.banner, div.pages, .replymode, .postblock, .passvalid, .catalogmode { background: #ff9100 url('img/testorange_f_bg.gif') repeat-x !important; } -div.pages { +div.pages, div.pages a { color: black; border: 0; } - -.abbrev { - color:#707070; +div.boardlist { + background-color: rgba(5%,5%,5%,0.2); } -.highlight { - background:#000; - border: 1px dashed #ff9100; +div.boardlist.bottom { + background-color:transparent; } -#watchedthreads { - background-color: #282828 !important; +.desktop-style div.boardlist:nth-child(1) { + text-shadow: #000 1px 1px 1px, #500 -1px -1px 1px; } -.reflinkpreview { - background-color: #282828 !important; + +.desktop-style div.boardlist:nth-child(1):hover { + background-color: rgba(0%, 0%, 0%, 0.45); } +p.intro span.subject { + color:#ee8100; +} + +span.quote { + color: #a8c952; +} diff --git a/stylesheets/wasabi.css b/stylesheets/wasabi.css index a080a12a..3da974c3 100644 --- a/stylesheets/wasabi.css +++ b/stylesheets/wasabi.css @@ -345,6 +345,8 @@ div.boardlist.bottom { } div.boardlist a { text-decoration: none; + color:#ffff22; + text-shadow: #000 1px 1px 1px, #000 -1px -1px 1px, #950000 1px 1px 2px, #009500 -1px -1px 2px; } div.report { color: #333; @@ -389,3 +391,12 @@ div.blotter { font-weight: bold; text-align: center; } + +.desktop-style div.boardlist:nth-child(1) { + +} + + +.desktop-style div.boardlist:nth-child(1):hover { + background-color: rgba(100%, 100%, 30%, 0.35); +} diff --git a/templates/attention_bar.html b/templates/attention_bar.html new file mode 100644 index 00000000..35f6b8f0 --- /dev/null +++ b/templates/attention_bar.html @@ -0,0 +1,6 @@ +{% if config.attention_bar %} +<div id="attention_bar"></div> +<form method="post" action="{{ config.root }}attentionbar.php" id="attention_bar_form"> + <input id="attention_bar_input" name="text" value="" type="text"> +</form> +{% endif %} diff --git a/templates/boardlist.html b/templates/boardlist.html new file mode 100644 index 00000000..62646845 --- /dev/null +++ b/templates/boardlist.html @@ -0,0 +1 @@ +I'm your overboard boardlist. You can put here anything and I reside in templates/boardlist.html diff --git a/templates/generic_page.html b/templates/generic_page.html index 8f608a6a..70cb15aa 100644 --- a/templates/generic_page.html +++ b/templates/generic_page.html @@ -20,6 +20,7 @@ </div> </header> + {% include 'attention_bar.html' %} {% include 'post_form.html' %} {% if config.global_message %}<hr /><div class="blotter">{{ config.global_message }}</div>{% endif %} diff --git a/templates/header.html b/templates/header.html index c1230f0e..d054fd6d 100644 --- a/templates/header.html +++ b/templates/header.html @@ -40,4 +40,4 @@ .recaptchatable, #recaptcha_area tr, #recaptcha_area td, #recaptcha_area th { padding: 0 !important; } - {% endraw %}</style>{% endif %}
\ No newline at end of file + {% endraw %}</style>{% endif %} diff --git a/templates/index.html b/templates/index.html index e32ac253..12150d0b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,6 +2,18 @@ <html> <head> <meta charset="utf-8"> + + <script type="text/javascript"> + {% if config.quick_reply %} + var new_reply_string = "{{ config.button_reply }}"; + {% endif %} + {% if not no_post_form %} + var active_page = "index"; + {% else %} + var active_page = "ukko"; + {% endif %} + </script> + {% include 'header.html' %} <title>{{ board.url }} - {{ board.title|e }}</title> </head> @@ -19,8 +31,13 @@ </div> </header> - {% include 'post_form.html' %} - + {% include 'attention_bar.html' %} + {% if not no_post_form %} + {% include 'post_form.html' %} + {% else %} + {% include 'boardlist.html' %} + {% endif %} + {% if config.global_message %}<hr /><div class="blotter">{{ config.global_message }}</div>{% endif %} <hr /> <form name="postcontrols" action="{{ config.post_url }}" method="post"> diff --git a/templates/post_form.html b/templates/post_form.html index 4d7a37b7..12f1186b 100644 --- a/templates/post_form.html +++ b/templates/post_form.html @@ -27,24 +27,22 @@ <td> <input type="text" name="email" size="25" maxlength="40" autocomplete="off"> {{ antibot.html() }} + {% if not (not (config.field_disable_subject or (id and config.field_disable_reply_subject)) or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri))) %} + <input accesskey="s" style="margin-left:2px;" type="submit" name="post" value="{% if id %}{{ config.button_reply }}{% else %}{{ config.button_newtopic }}{% endif %}" />{% if config.spoiler_images %} <input id="spoiler" name="spoiler" type="checkbox"> <label for="spoiler">{% trans %}Spoiler Image{% endtrans %}</label>{% endif %} + {% endif %} </td> </tr>{% endif %} - <tr> - {% if not (config.field_disable_subject or (id and config.field_disable_reply_subject)) or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<th> + {% if not (config.field_disable_subject or (id and config.field_disable_reply_subject)) or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr> + <th> {% trans %}Subject{% endtrans %} {{ antibot.html() }} </th> <td> <input style="float:left;" type="text" name="subject" size="25" maxlength="100" autocomplete="off"> - {% else %}<th> - {% trans %}Submit{% endtrans %} - {{ antibot.html() }} - </th> - <td> - {% endif %} <input accesskey="s" style="margin-left:2px;" type="submit" name="post" value="{% if id %}{{ config.button_reply }}{% else %}{{ config.button_newtopic }}{% endif %}" />{% if config.spoiler_images %} <input id="spoiler" name="spoiler" type="checkbox"> <label for="spoiler">{% trans %}Spoiler Image{% endtrans %}</label>{% endif %} </td> </tr> + {% endif %} <tr> <th> {% trans %}Comment{% endtrans %} @@ -53,6 +51,11 @@ <td> <textarea name="body" id="body" rows="5" cols="35"></textarea> {{ antibot.html() }} + {% if not (not (config.field_disable_subject or (id and config.field_disable_reply_subject)) or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri))) %} + {% if not (not config.field_disable_email or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri))) %} + <input accesskey="s" style="margin-left:2px;" type="submit" name="post" value="{% if id %}{{ config.button_reply }}{% else %}{{ config.button_newtopic }}{% endif %}" />{% if config.spoiler_images %} <input id="spoiler" name="spoiler" type="checkbox"> <label for="spoiler">{% trans %}Spoiler Image{% endtrans %}</label>{% endif %} + {% endif %} + {% endif %} </td> </tr> {% if config.recaptcha %} diff --git a/templates/post_thread.html b/templates/post_thread.html index 21add308..a24d7557 100644 --- a/templates/post_thread.html +++ b/templates/post_thread.html @@ -1,7 +1,7 @@ {% filter remove_whitespace %} {# tabs and new lines will be ignored #} -<div id="thread_{{ post.id }}"> +<div id="thread_{{ post.id }}" data-board="{{ board.uri }}"> {% if post.embed %} {{ post.embed }} diff --git a/templates/search_form.html b/templates/search_form.html new file mode 100644 index 00000000..4c8bd077 --- /dev/null +++ b/templates/search_form.html @@ -0,0 +1,19 @@ +<div class="ban"> + <h2>{% trans %}Search{% endtrans %}</h2> + <form style="display:inline" action="" method="get"> + <p> + <label style="display:inline" for="search">{% trans %}Phrase:{% endtrans %}</label> + <input id="search" name="search" type="text" size="40" value="{{ search }}"> + <select name="board"> + <option value="none">{% trans %}Select board{% endtrans %}…</option> + {% for b in boards %} + <option value="{{ b }}"{% if b == board %} selected{% endif %}>/{{ b }}/</option> + {% endfor %} + </select> + <input type="submit" value="{% trans %}Search{% endtrans %}" /> + </p> + </form> + <p style="font-size:8pt;margin:5px"> + {% trans %}Search is case-insensitive and based on keywords. To match exact phrases, use "quotes". Use an asterisk (*) for wildcard.</p><p style="font-size:8pt;margin:5px">You may apply the following filters to your searches: <strong>id</strong>, <strong>thread</strong>, <strong>subject</strong>, and <strong>name</strong>. To apply a filter, simply add to your query, for example, <em>name:Anonymous</em> or <em>subject:"Some Thread"</em>. Wildcards cannot be used in filters.{% endtrans %} + </p> +</div> diff --git a/templates/themes/ukko/info.php b/templates/themes/ukko/info.php new file mode 100644 index 00000000..34d59d4e --- /dev/null +++ b/templates/themes/ukko/info.php @@ -0,0 +1,53 @@ +<?php + $theme = Array(); + + // Theme name + $theme['name'] = 'Ukko'; + // Description (you can use Tinyboard markup here) + $theme['description'] = 'Board with threads and messages from all boards'; + $theme['version'] = 'v0.1'; + + // Theme configuration + $theme['config'] = Array(); + + $theme['config'][] = Array( + 'title' => 'Board name', + 'name' => 'title', + 'type' => 'text', + 'default' => 'Ukko' + ); + $theme['config'][] = Array( + 'title' => 'Board URI', + 'name' => 'uri', + 'type' => 'text', + 'comment' => '(ukko for example)' + ); + $theme['config'][] = Array( + 'title' => 'Subtitle', + 'name' => 'subtitle', + 'type' => 'text', + 'comment' => '(%s = thread limit. for example "%s freshly bumped threads")' + ); + $theme['config'][] = Array( + 'title' => 'Excluded boards', + 'name' => 'exclude', + 'type' => 'text', + 'comment' => '(space seperated)' + ); + $theme['config'][] = Array( + 'title' => 'Number of threads', + 'name' => 'thread_limit', + 'type' => 'text', + 'default' => '15', + ); + // Unique function name for building everything + $theme['build_function'] = 'ukko_build'; + $theme['install_callback'] = 'ukko_install'; + + if(!function_exists('ukko_install')) { + function ukko_install($settings) { + if (!file_exists($settings['uri'])) + @mkdir($settings['uri'], 0777) or error("Couldn't create " . $settings['uri'] . ". Check permissions.", true); + } + } + diff --git a/templates/themes/ukko/theme.php b/templates/themes/ukko/theme.php new file mode 100644 index 00000000..1682b4ec --- /dev/null +++ b/templates/themes/ukko/theme.php @@ -0,0 +1,111 @@ +<?php + require 'info.php'; + + function ukko_build($action, $settings) { + $ukko = new ukko(); + $ukko->settings = $settings; + $ukko->build(); + } + + class ukko { + public $settings; + public function build($mod = false) { + global $config; + $boards = listBoards(); + + $body = ''; + $overflow = array(); + $board = array( + 'url' => $this->settings['uri'], + 'name' => $this->settings['title'], + 'title' => sprintf($this->settings['subtitle'], $this->settings['thread_limit']) + ); + + $query = ''; + foreach($boards as &$_board) { + if(in_array($_board['uri'], explode(' ', $this->settings['exclude']))) + continue; + $query .= sprintf("SELECT *, '%s' AS `board` FROM ``posts_%s`` WHERE `thread` IS NULL UNION ALL ", $_board['uri'], $_board['uri']); + } + $query = preg_replace('/UNION ALL $/', 'ORDER BY `bump` DESC', $query); + $query = query($query) or error(db_error()); + + $count = 0; + $threads = array(); + while($post = $query->fetch()) { + + if(!isset($threads[$post['board']])) { + $threads[$post['board']] = 1; + } else { + $threads[$post['board']] += 1; + } + + if($count < $this->settings['thread_limit']) { + openBoard($post['board']); + $thread = new Thread( + $post['id'], $post['subject'], $post['email'], $post['name'], $post['trip'], $post['capcode'], $post['body'], $post['time'], + $post['thumb'], $post['thumbwidth'], $post['thumbheight'], $post['file'], $post['filewidth'], $post['fileheight'], $post['filesize'], + $post['filename'], $post['ip'], $post['sticky'], $post['locked'], $post['sage'], $post['embed'], $mod ? '?/' : $config['root'], $mod + ); + + $posts = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE `thread` = :id ORDER BY `id` DESC LIMIT :limit", $post['board'])); + $posts->bindValue(':id', $post['id']); + $posts->bindValue(':limit', ($post['sticky'] ? $config['threads_preview_sticky'] : $config['threads_preview']), PDO::PARAM_INT); + $posts->execute() or error(db_error($posts)); + + $num_images = 0; + while ($po = $posts->fetch()) { + if ($po['file']) + $num_images++; + + $thread->add(new Post( + $po['id'], $post['id'], $po['subject'], $po['email'], $po['name'], $po['trip'], $po['capcode'], $po['body'], $po['time'], + $po['thumb'], $po['thumbwidth'], $po['thumbheight'], $po['file'], $po['filewidth'], $po['fileheight'], $po['filesize'], + $po['filename'], $po['ip'], $po['embed'], $mod ? '?/' : $config['root'], $mod) + ); + + } + if ($posts->rowCount() == ($post['sticky'] ? $config['threads_preview_sticky'] : $config['threads_preview'])) { + $ct = prepare(sprintf("SELECT COUNT(`id`) as `num` FROM ``posts_%s`` WHERE `thread` = :thread UNION ALL SELECT COUNT(`id`) FROM `posts_%s` WHERE `file` IS NOT NULL AND `thread` = :thread", $post['board'], $post['board'])); + $ct->bindValue(':thread', $post['id'], PDO::PARAM_INT); + $ct->execute() or error(db_error($count)); + + $c = $ct->fetch(); + $thread->omitted = $c['num'] - ($post['sticky'] ? $config['threads_preview_sticky'] : $config['threads_preview']); + + $c = $ct->fetch(); + $thread->omitted_images = $c['num'] - $num_images; + } + + + $thread->posts = array_reverse($thread->posts); + $body .= '<h2><a href="' . $config['root'] . $post['board'] . '">/' . $post['board'] . '/</a></h2>'; + $body .= $thread->build(true); + } else { + $page = 'index'; + if(floor($threads[$post['board']] / $config['threads_per_page']) > 0) { + $page = floor($threads[$post['board']] / $config['threads_per_page']) + 1; + } + $overflow[] = array('id' => $post['id'], 'board' => $post['board'], 'page' => $page . '.html'); + } + + $count += 1; + } + + $body .= '<script> var overflow = ' . json_encode($overflow) . '</script>'; + $body .= '<script type="text/javascript" src="ukko.js"></script>'; + + file_write($this->settings['uri'] . '/index.html', Element('index.html', array( + 'config' => $config, + 'board' => $board, + 'no_post_form' => true, + 'body' => $body, + 'boardlist' => createBoardlist($mod) + ))); + + file_write($this->settings['uri'] . '/ukko.js', Element('themes/ukko/ukko.js', array())); + } + + }; + +?> diff --git a/templates/themes/ukko/thumb.png b/templates/themes/ukko/thumb.png Binary files differnew file mode 100644 index 00000000..49c4237b --- /dev/null +++ b/templates/themes/ukko/thumb.png diff --git a/templates/themes/ukko/ukko.js b/templates/themes/ukko/ukko.js new file mode 100644 index 00000000..d8693db3 --- /dev/null +++ b/templates/themes/ukko/ukko.js @@ -0,0 +1,110 @@ +(function(){ + +var cache = new Array(), + thread = false, + loading = false; + +if (localStorage.hiddenboards !== null) { + localStorage.hiddenboards = "{}"; +} + +// Load data from HTML5 localStorage +var hiddenboards = JSON.parse(localStorage.hiddenboards); + +var storeboards = function() { + localStorage.hiddenboards = JSON.stringify(hiddenboards); +}; + +$(document).ready(function() { + var addukkohide = function() { + var ukkohide = $('<a href="javascript:void(0);" class="unimportant ukkohide"></a>'); + var board = $(this).next().data("board"); + var hr = $("<hr />"); + + $(this).append(ukkohide); + $(this).append(hr); + + if (hiddenboards[board] !== true) { + ukkohide.html(_("(hide threads from this board)")); + hr.hide(); + } + else { + ukkohide.html(_("(show threads from this board)")); + $(this).next().hide(); + } + ukkohide.click(function() { + hiddenboards[board] = (hiddenboards[board] !== true); + if (hiddenboards[board] !== true) { + $('[data-board="'+board+'"]:not([data-cached="yes"])').show().prev(). + find('.ukkohide').html(_("(hide threads from this board)")). + parent().find('hr').hide(); + } + else { + $('[data-board="'+board+'"]:not([data-cached="yes"])').hide().prev(). + find('.ukkohide').html(_("(show threads from this board)")) + .parent().find('hr').show(); + } + storeboards(); + return false; + }); + + }; + $("h2").each(addukkohide); + + $('.pages').hide(); + $(window).on('scroll', function() { + if (overflow.length == 0) { + $('.pages').show().html(_("No more threads to display")); + } + while($(window).scrollTop() + $(window).height() + 500 > $(document).height() && !loading && overflow.length > 0) { + var page = '../' + overflow[0].board + '/' + overflow[0].page; + thread = $('div#thread_' + overflow[0].id + '[data-board="' + overflow[0].board + '"]'); + if (thread.length > 0 && thread.attr("data-cached") !== 'yes') { // already present + overflow.shift(); + continue; + } + + var boardheader = $('<h2><a href="/' + overflow[0].board + '/">/' + overflow[0].board + '/</a> </h2>'); + + if($.inArray(page, cache) != -1 && thread.length > 0) { + $('div[id*="thread_"]').last().after(thread.attr('data-board', overflow[0].board).attr("data-cached", "no").css('display', 'block')); + boardheader.insertBefore(thread); + addukkohide.call(boardheader); + $(document).trigger('new_post', thread); + overflow.shift(); + } else { + loading = true; + $('.pages').show().html(_("Loading...")); + $.get(page, function(data) { + cache.push(page); + + $(data).find('div[id*="thread_"]').each(function() { + var checkout = $(this).attr('id').replace('thread_', ''); + if ($('div#thread_' + checkout + '[data-board="' + overflow[0].board + '"]').length == 0) { + $('form[name="postcontrols"]').prepend($(this).css('display', 'none').attr("data-cached", "yes").attr('data-board', overflow[0].board)); + } + }); + + thread = $('div#thread_' + overflow[0].id + '[data-board="' + overflow[0].board + '"][data-cached="yes"]'); + + if(thread.length > 0) { + $('div[id*="thread_"]').last().after(thread.attr('data-board', overflow[0].board).attr("data-cached", "no").css('display', 'block')); + boardheader.insertBefore(thread); + addukkohide.call(boardheader); + $(document).trigger('new_post', thread); + overflow.shift(); + } + else { + overflow.shift(); // We missed it? Or already present... + } + + loading = false; + $('.pages').hide().html(""); + }); + break; + } + } + }); +}); + +})(); diff --git a/templates/thread.html b/templates/thread.html index 97f8a3db..7fe60a39 100644 --- a/templates/thread.html +++ b/templates/thread.html @@ -2,6 +2,11 @@ <html> <head> <meta charset="utf-8"> + + <script type="text/javascript"> + var active_page = "thread"; + </script> + {% include 'header.html' %} <title>{{ board.url }} - {% if config.thread_subject_in_title and thread.subject %}{{ thread.subject }}{% else %}{{ board.title|e }}{% endif %}</title> </head> @@ -21,6 +26,8 @@ <div class="banner">{% trans %}Posting mode: Reply{% endtrans %} <a class="unimportant" href="{{ return }}">[{% trans %}Return{% endtrans %}]</a></div> + {% include 'attention_bar.html' %} + {% include 'post_form.html' %} {% if config.global_message %}<hr /><div class="blotter">{{ config.global_message }}</div>{% endif %} @@ -42,4 +49,4 @@ ready(); {% endraw %}</script> </body> -</html>
\ No newline at end of file +</html> diff --git a/tools/benchmark.php b/tools/benchmark.php new file mode 100644 index 00000000..8c97364c --- /dev/null +++ b/tools/benchmark.php @@ -0,0 +1,52 @@ +#!/usr/bin/php +<?php +/* + * benchmark.php - benchmarks thumbnailing methods + * + */ + +require dirname(__FILE__) . '/inc/cli.php'; +require 'inc/image.php'; + +// move back to this directory +chdir(dirname(__FILE__)); + +if(count($argv) != 2) + die("Usage: {$argv[0]} [file]\n"); + +$file = $argv[1]; +$extension = strtolower(substr($file, strrpos($file, '.') + 1)); +$out = tempnam($config['tmp'], 'thumb'); +$count = 300; + +function benchmark($method) { + global $config, $file, $extension, $out, $count; + + $config['thumb_method'] = $method; + + printf("Method: %s\nThumbnailing %d times... ", $method, $count); + + $start = microtime(true); + for($i = 0; $i < $count; $i++) { + $image = new Image($file, $extension); + $thumb = $image->resize( + $config['thumb_ext'] ? $config['thumb_ext'] : $extension, + $config['thumb_width'], + $config['thumb_height'] + ); + + $thumb->to($out); + $thumb->_destroy(); + $image->destroy(); + } + $end = microtime(true); + + printf("Took %.2f seconds (%.2f/second; %.2f ms)\n", $end - $start, $rate = ($count / ($end - $start)), 1000 / $rate); + + unlink($out); +} + +benchmark('gd'); +benchmark('imagick'); +benchmark('convert'); + diff --git a/tools/i18n_compile.php b/tools/i18n_compile.php new file mode 100755 index 00000000..ba193767 --- /dev/null +++ b/tools/i18n_compile.php @@ -0,0 +1,44 @@ +#!/usr/bin/php +<?php + +/* + * i18n_compile.php - compiles the i18n + * + * Options: + * -l [locale], --locale=[locale] + * Compiles [locale] locale. + * + */ + +require dirname(__FILE__) . '/inc/cli.php'; + +// parse command line +$opts = getopt('l:', Array('locale:')); +$options = Array(); + +$options['locale'] = isset($opts['l']) ? $opts['l'] : (isset($opts['locale']) ? $opts['locale'] : false); + +if ($options['locale']) $locales = array($options['locale']); +else die("Error: no locales specified; use -l switch, eg. -l pl_PL\n"); + +foreach ($locales as $loc) { + if (file_exists ($locdir = "inc/locale/".$loc)) { + if (!is_dir ($locdir)) { + continue; + } + } + else { + die("Error: $locdir does not exist\n"); + } + + // Generate tinyboard.po + if (file_exists($locdir."/LC_MESSAGES/tinyboard.po")) $join = "-j"; + else $join = ""; + passthru("cd $locdir/LC_MESSAGES; + msgfmt tinyboard.po -o tinyboard.mo"); + + // Generate javascript.po + passthru("cd tools/inc/lib/jsgettext/; + php po2json.php -i ../../../../$locdir/LC_MESSAGES/javascript.po \ + -o ../../../../$locdir/LC_MESSAGES/javascript.js"); +} diff --git a/tools/i18n_extract.php b/tools/i18n_extract.php new file mode 100755 index 00000000..33650b50 --- /dev/null +++ b/tools/i18n_extract.php @@ -0,0 +1,47 @@ +#!/usr/bin/php +<?php + +/* + * i18n_extract.php - extracts the strings and updates all locales + * + * Options: + * -l [locale], --locale=[locale] + * Updates only [locale] locale. If it does not exist yet, we create a new directory. + * + */ + +require dirname(__FILE__) . '/inc/cli.php'; + +// parse command line +$opts = getopt('l:', Array('locale:')); +$options = Array(); + +$options['locale'] = isset($opts['l']) ? $opts['l'] : (isset($opts['locale']) ? $opts['locale'] : false); + +$locales = glob("inc/locale/*"); +$locales = array_map("basename", $locales); + +if ($options['locale']) $locales = array($options['locale']); + + +foreach ($locales as $loc) { + if (file_exists ($locdir = "inc/locale/".$loc)) { + if (!is_dir ($locdir)) { + continue; + } + } + else { + mkdir($locdir); + mkdir($locdir."/LC_MESSAGES"); + } + + // Generate tinyboard.po + if (file_exists($locdir."/LC_MESSAGES/tinyboard.po")) $join = "-j"; + else $join = ""; + passthru("cd $locdir/LC_MESSAGES; + xgettext -d tinyboard -L php --from-code utf-8 $join -c $(find ../../../../ -name \*.php)"); + + // Generate javascript.po + passthru("cd $locdir/LC_MESSAGES; + xgettext -d javascript -L Python --force-po --from-code utf-8 $join -c $(find ../../../../js/ -name \*.js)"); +} diff --git a/tools/inc/cli.php b/tools/inc/cli.php new file mode 100644 index 00000000..294609cd --- /dev/null +++ b/tools/inc/cli.php @@ -0,0 +1,100 @@ +<?php + +/* + * This script will look for Tinyboard in the following places (in order): + * - $TINYBOARD_PATH environment varaible + * - ./ + * - ./Tinyboard/ + * - ../ + */ + +set_time_limit(0); +$shell_path = getcwd(); + +if(getenv('TINYBOARD_PATH') !== false) + $dir = getenv('TINYBOARD_PATH'); +elseif(file_exists('inc/functions.php')) + $dir = false; +elseif(file_exists('Tinyboard') && is_dir('Tinyboard') && file_exists('Tinyboard/inc/functions.php')) + $dir = 'Tinyboard'; +elseif(file_exists('../inc/functions.php')) + $dir = '..'; +else + die("Could not locate Tinyboard directory!\n"); + +if($dir && !chdir($dir)) + die("Could not change directory to {$dir}\n"); + +if(!getenv('TINYBOARD_PATH')) { + // follow symlink + chdir(realpath('inc') . '/..'); +} + +putenv('TINYBOARD_PATH=' . getcwd()); + +require 'inc/functions.php'; +require 'inc/mod.php'; + +$mod = Array( + 'id' => -1, + 'type' => ADMIN, + 'username' => '?', + 'boards' => Array('*') +); + +function get_httpd_privileges() { + global $config, $shell_path, $argv; + + if(php_sapi_name() != 'cli') + die("get_httpd_privileges(): invoked from HTTP client.\n"); + + echo "Dropping priviledges...\n"; + + if(!is_writable('.')) + die("get_httpd_privileges(): web directory is not writable\n"); + + $filename = '.' . md5(rand()) . '.php'; + $inc_filename = '.' . md5(rand()) . '.php'; + + echo "Copying rebuilder to web directory...\n"; + + // replace "/inc/cli.php" with its new filename + passthru("cat " . escapeshellarg($shell_path . '/' . $_SERVER['PHP_SELF']) . " | sed \"s/'\/inc\/cli\.php'/'\/{$inc_filename}'/\" > {$filename}"); + + $inc_header = "<?php\n"; + + // copy environment + $env = explode("\n", shell_exec('printenv | grep ^TINYBOARD')); + foreach($env as $line) { + if(!empty($line)) + $inc_header .= "putenv('" . addslashes($line) . "');\n"; + } + + // copy command line arguments + $inc_header .= "\$argv = " . var_export($argv, true) . ";\n"; + + // copy this file + file_put_contents($inc_filename, $inc_header . substr($inc = file_get_contents(__FILE__), strpos($inc, "\n"))); + + chmod($filename, 0666); + chmod($inc_filename, 0666); + + if(preg_match('/^https?:\/\//', $config['root'])) { + $url = $config['root'] . $filename; + } elseif($host = getenv('TINYBOARD_HOST')) { + $url = 'http://' . $host . $config['root'] . $filename; + } else { + // assume localhost + $url = 'http://localhost' . $config['root'] . $filename; + } + + echo "Downloading $url\n"; + + passthru('curl -s -N ' . escapeshellarg($url)); + + unlink($filename); + unlink($inc_filename); + + exit(0); +} + diff --git a/tools/inc/lib/jsgettext/JSParser.php b/tools/inc/lib/jsgettext/JSParser.php new file mode 100644 index 00000000..adba59bc --- /dev/null +++ b/tools/inc/lib/jsgettext/JSParser.php @@ -0,0 +1,94 @@ +<?php + +class JSParser { + + protected $content; + protected $keywords; + protected $regs = array(); + protected $regsCounter = 0; + protected $strings = array(); + protected $stringsCounter = 0; + + protected function _extractRegs($match) { + $this->regs[$this->regsCounter] = $match[1]; + $id = "<<reg{$this->regsCounter}>>"; + $this->regsCounter++; + return $id; + } + protected function _extractStrings($match) { + $this->strings[$this->stringsCounter] = $this->importRegExps($match[0]); + $id = "<<s{$this->stringsCounter}>>"; + $this->stringsCounter++; + return $id; + } + protected function importRegExps($input) { + $regs = $this->regs; + return preg_replace_callback("#<<reg(\d+)>>#", function ($match) use($regs) { + return $regs[$match[1]]; + }, $input); + } + + protected function importStrings($input) { + $strings = $this->strings; + return preg_replace_callback("#<<s(\d+)>>#", function ($match) use($strings) { + return $strings[$match[1]]; + }, $input); + } + + public function __construct($file, $keywords = '_') { + $this->content = file_get_contents($file); + $this->keywords = (array)$keywords; + } + + public function parse() { + $output = $this->content; //htmlspecialchars($this->content, ENT_NOQUOTES); + + // extract reg exps + $output = preg_replace_callback( + '# ( / (?: (?>[^/\\\\]++) | \\\\\\\\ | (?<!\\\\)\\\\(?!\\\\) | \\\\/ )+ (?<!\\\\)/ ) [a-z]* \b #ix', + array($this, '_extractRegs'), $output + ); + + // extract strings + $output = preg_replace_callback( + array( + '# " ( (?: (?>[^"\\\\]++) | \\\\\\\\ | (?<!\\\\)\\\\(?!\\\\) | \\\\" )* ) (?<!\\\\)" #ix', + "# ' ( (?: (?>[^'\\\\]++) | \\\\\\\\ | (?<!\\\\)\\\\(?!\\\\) | \\\\' )* ) (?<!\\\\)' #ix" + ), array($this, '_extractStrings'), $output + ); + + // delete line comments + $output = preg_replace("#(//.*?)$#m", '', $output); + + // delete multiline comments + $output = preg_replace('#/\*(.*?)\*/#is', '', $output); + + $strings = $this->strings; + $output = preg_replace_callback("#<<s(\d+)>>#", function($match) use($strings) { + return $strings[$match[1]]; + }, $output); + + $keywords = implode('|', $this->keywords); + + $strings = array(); + + // extract func calls + preg_match_all( + '# (?:'.$keywords.') \(\\ *" ( (?: (?>[^"\\\\]++) | \\\\\\\\ | (?<!\\\\)\\\\(?!\\\\) | \\\\" )* ) (?<!\\\\)"\\ *\) #ix', + $output, $matches, PREG_SET_ORDER + ); + + foreach ($matches as $m) $strings[] = stripslashes($m[1]); + + $matches = array(); + preg_match_all( + "# (?:$keywords) \(\\ *' ( (?: (?>[^'\\\\]++) | \\\\\\\\ | (?<!\\\\)\\\\(?!\\\\) | \\\\' )* ) (?<!\\\\)'\\ *\) #ix", + $output, $matches, PREG_SET_ORDER + ); + + foreach ($matches as $m) $strings[] = stripslashes($m[1]); + + return $strings; + } +} +?> diff --git a/tools/inc/lib/jsgettext/PoeditParser.php b/tools/inc/lib/jsgettext/PoeditParser.php new file mode 100644 index 00000000..eebd816e --- /dev/null +++ b/tools/inc/lib/jsgettext/PoeditParser.php @@ -0,0 +1,83 @@ +<?php + +require_once 'PoeditString.php'; + +class PoeditParser { + + protected $file; + protected $header = ''; + protected $strings = array(); + + protected function _fixQuotes($str) { + return stripslashes($str); + } + + public function __construct($file) { + $this->file = $file; + } + + public function parse() { + $contents = file_get_contents($this->file); + + $parts = preg_split('#(\r\n|\n){2}#', $contents, -1, PREG_SPLIT_NO_EMPTY); + + $this->header = array_shift($parts); + + foreach ($parts as $part) { + + // parse comments + $comments = array(); + preg_match_all('#^\\#: (.*?)$#m', $part, $matches, PREG_SET_ORDER); + foreach ($matches as $m) $comments[] = $m[1]; + + $isFuzzy = preg_match('#^\\#, fuzzy$#im', $part) ? true : false; + + preg_match_all('# ^ (msgid|msgstr)\ " ( (?: (?>[^"\\\\]++) | \\\\\\\\ | (?<!\\\\)\\\\(?!\\\\) | \\\\" )* ) (?<!\\\\)" $ #ixm', $part, $matches2, PREG_SET_ORDER); + + $k = $this->_fixQuotes($matches2[0][2]); + $v = !empty($matches2[1][2]) ? $this->_fixQuotes($matches2[1][2]) : ''; + + $this->strings[$k] = new PoeditString($k, $v, $isFuzzy, $comments); + } + } + + public function merge($strings) { + foreach ((array)$strings as $str) { + if (!in_array($str, array_keys($this->strings))) { + $this->strings[$str] = new PoeditString($str); + } + } + } + + public function getHeader() { + return $this->header; + } + + public function getStrings() { + return $this->strings; + } + + public function getJSON() { + $str = array(); + foreach ($this->strings as $s) { + if ($s->value) $str[$s->key] = $s->value; + } + return json_encode($str); + } + + public function toJSON($outputFilename, $varName = 'l10n') { + $str = "$varName = " . $this->getJSON() . ";"; + return file_put_contents($outputFilename, $str) !== false; + } + + public function save($filename = null) { + $data = $this->header . "\n\n"; + foreach ($this->strings as $str) { + $data .= $str; + } + return file_put_contents($filename ? $filename : $this->file, $data) !== false; + } +} + + +?> diff --git a/tools/inc/lib/jsgettext/PoeditString.php b/tools/inc/lib/jsgettext/PoeditString.php new file mode 100644 index 00000000..3d59d004 --- /dev/null +++ b/tools/inc/lib/jsgettext/PoeditString.php @@ -0,0 +1,29 @@ +<?php + +class PoeditString { + public $key; + public $value; + public $fuzzy; + public $comments; + + function __construct($key, $value = '', $fuzzy = false, $comments = array()) { + $this->key = $key; + $this->value = $value; + $this->fuzzy = $fuzzy; + $this->comments = (array)$comments; + } + + public function __toString() { + $str =''; + foreach ($this->comments as $c) { + $str .= "#: $c\n"; + } + if ($this->fuzzy) $str .= "#, fuzzy\n"; + $str .= 'msgid "'.str_replace('"', '\\"', $this->key).'"' . "\n"; + $str .= 'msgstr "'.str_replace('"', '\\"', $this->value).'"' . "\n"; + $str .= "\n"; + return $str; + } +} + +?>
\ No newline at end of file diff --git a/tools/inc/lib/jsgettext/jsgettext.php b/tools/inc/lib/jsgettext/jsgettext.php new file mode 100644 index 00000000..6c70f0b4 --- /dev/null +++ b/tools/inc/lib/jsgettext/jsgettext.php @@ -0,0 +1,63 @@ +<?php + + require_once 'JSParser.php'; + require_once 'PoeditParser.php'; + + function buildOptions($args) { + $options = array( + 'files' => array(), + '-o' => null, + '-k' => '_' + ); + $len = count($args); + $i = 1; + while ($i < $len) { + if (preg_match('#^-[a-z]$#i', $args[$i])) { + $options[$args[$i]] = isset($args[$i+1]) ? trim($args[$i+1]) : true; + $i += 2; + } + else { + $options['files'][] = $args[$i]; + $i++; + } + } + return $options; + } + + $options = buildOptions($argv); + + if (!file_exists($options['-o'])) { + touch($options['-o']); + } + + if (!is_writable($options['-o'])) { + die("Invalid output file name. Make sure it exists and is writable."); + } + + $inputFiles = $options['files']; + + if (empty($inputFiles)) { + die("You did not provide any input file."); + } + + $poeditParser = new PoeditParser($options['-o']); + $poeditParser->parse(); + + $errors = array(); + + foreach ($inputFiles as $f) { + if (!is_readable($f) || !preg_match('#\.js$#', $f)) { + $errors[] = ("$f is not a valid javascript file."); + continue; + } + $jsparser = new JSParser($f, explode(' ', $options['-k'])); + $jsStrings = $jsparser->parse(); + $poeditParser->merge($jsStrings); + } + + if (!empty($errors)) { + echo "\nThe following errors occured:\n" . implode("\n", $errors) . "\n"; + } + + $poeditParser->save(); +?> diff --git a/tools/inc/lib/jsgettext/po2json.php b/tools/inc/lib/jsgettext/po2json.php new file mode 100644 index 00000000..847b0899 --- /dev/null +++ b/tools/inc/lib/jsgettext/po2json.php @@ -0,0 +1,42 @@ +<?php + + require_once 'PoeditParser.php'; + + function buildOptions($args) { + $options = array( + '-o' => null, + '-i' => null, + '-n' => 'l10n' + ); + $len = count($args); + $i = 0; + while ($i < $len) { + if (preg_match('#^-[a-z]$#i', $args[$i])) { + $options[$args[$i]] = isset($args[$i+1]) ? trim($args[$i+1]) : true; + $i += 2; + } + else { + $options[] = $args[$i]; + $i++; + } + } + return $options; + } + + $options = buildOptions($argv); + + if (!file_exists($options['-i']) || !is_readable($options['-i'])) { + die("Invalid input file. Make sure it exists and is readable."); + } + + $poeditParser = new PoeditParser($options['-i']); + $poeditParser->parse(); + + if ($poeditParser->toJSON($options['-o'], $options['-n'])) { + $strings = count($poeditParser->getStrings()); + echo "Successfully exported " . count($strings) . " strings.\n"; + } + else { + echo "Cannor write to file '{$options['-o']}'.\n"; + } +?> diff --git a/tools/rebuild.php b/tools/rebuild.php new file mode 100755 index 00000000..3b5c0de2 --- /dev/null +++ b/tools/rebuild.php @@ -0,0 +1,105 @@ +#!/usr/bin/php +<?php + +/* + * rebuild.php - rebuilds all static files + * + * Command line arguments: + * -q, --quiet + * Suppress output. + * + * --quick + * Do not rebuild posts. + * + * -b, --board <string> + * Rebuild only the specified board. + * + * -f, --full + * Rebuild replies as well as threads (re-markup). + * + */ + +require dirname(__FILE__) . '/inc/cli.php'; + +if(!is_writable($config['file_script'])) { + get_httpd_privileges(); +} + +$start = microtime(true); + +// parse command line +$opts = getopt('qfb:', Array('board:', 'quick', 'full', 'quiet')); +$options = Array(); + +$options['board'] = isset($opts['board']) ? $opts['board'] : (isset($opts['b']) ? $opts['b'] : false); +$options['quiet'] = isset($opts['q']) || isset($opts['quiet']); +$options['quick'] = isset($opts['quick']); +$options['full'] = isset($opts['full']) || isset($opts['f']); + +if(!$options['quiet']) + echo "== Tinyboard {$config['version']} ==\n"; + +if(!$options['quiet']) + echo "Clearing template cache...\n"; + +load_twig(); +$twig->clearCacheFiles(); + +if(!$options['quiet']) + echo "Regenerating theme files...\n"; +rebuildThemes('all'); + +if(!$options['quiet']) + echo "Generating Javascript file...\n"; +buildJavascript(); + +$main_js = $config['file_script']; + +$boards = listBoards(); + +foreach($boards as &$board) { + if($options['board'] && $board['uri'] != $options['board']) + continue; + + if(!$options['quiet']) + echo "Opening board /{$board['uri']}/...\n"; + openBoard($board['uri']); + + if($config['file_script'] != $main_js) { + // different javascript file + if(!$options['quiet']) + echo "Generating Javascript file...\n"; + buildJavascript(); + } + + + if(!$options['quiet']) + echo "Creating index pages...\n"; + buildIndex(); + + if($options['quick']) + continue; // do no more + + if($options['full']) { + $query = query(sprintf("SELECT `id` FROM `posts_%s`", $board['uri'])) or error(db_error()); + while($post = $query->fetch()) { + if(!$options['quiet']) + echo "Rebuilding #{$post['id']}...\n"; + rebuildPost($post['id']); + } + } + + $query = query(sprintf("SELECT `id` FROM `posts_%s` WHERE `thread` IS NULL", $board['uri'])) or error(db_error()); + while($post = $query->fetch()) { + if(!$options['quiet']) + echo "Rebuilding #{$post['id']}...\n"; + buildThread($post['id']); + } +} + +if(!$options['quiet']) + printf("Complete! Took %g seconds\n", microtime(true) - $start); + +unset($board); +modLog('Rebuilt everything using tools/rebuild.php'); + |
