diff options
| -rw-r--r-- | inc/config.php | 1043 | ||||
| -rw-r--r-- | inc/database.php | 12 | ||||
| -rw-r--r-- | inc/display.php | 70 | ||||
| -rw-r--r-- | inc/filters.php | 4 | ||||
| -rw-r--r-- | inc/functions.php | 628 | ||||
| -rw-r--r-- | inc/image.php | 28 | ||||
| -rw-r--r-- | inc/mod/config-editor.php | 53 | ||||
| -rw-r--r-- | inc/mod/pages.php | 73 | ||||
| -rw-r--r-- | install.php | 184 | ||||
| -rw-r--r-- | mod.php | 84 | ||||
| -rw-r--r-- | post.php | 11 | ||||
| -rw-r--r-- | stylesheets/gentoochan.css | 9 | ||||
| -rw-r--r-- | stylesheets/ricechan.css | 2 | ||||
| -rw-r--r-- | templates/error.html | 18 | ||||
| -rw-r--r-- | templates/generic_page.html | 3 | ||||
| -rw-r--r-- | templates/index.html | 2 | ||||
| -rw-r--r-- | templates/installer/check-requirements.html | 10 | ||||
| -rw-r--r-- | templates/installer/config.html | 95 | ||||
| -rw-r--r-- | templates/mod/config-editor-php.html | 44 | ||||
| -rw-r--r-- | templates/mod/config-editor.html | 29 | ||||
| -rw-r--r-- | templates/post_form.html | 3 | ||||
| -rw-r--r-- | templates/thread.html | 2 |
22 files changed, 1302 insertions, 1105 deletions
diff --git a/inc/config.php b/inc/config.php index dfdbbf2f..4742fca9 100644 --- a/inc/config.php +++ b/inc/config.php @@ -29,45 +29,49 @@ * General/misc settings * ======================= */ - // Blotter -- the simple version. - //$config['blotter'] = 'This is an important announcement!'; - - // Automatically check if a newer version of Tinyboard is available when an administrator logs in + + // Global announcement -- the very simple version. + // This used to be wrongly named $config['blotter'] (still exists as an alias). + // $config['global_message'] = 'This is an important announcement!'; + $config['blotter'] = &$config['global_message']; + + // Automatically check if a newer version of Tinyboard is available when an administrator logs in. $config['check_updates'] = true; // How often to check for updates $config['check_updates_time'] = 43200; // 12 hours - - // Shows some extra information at the bottom of pages. Good for debugging development. + + // Shows some extra information at the bottom of pages. Good for development/debugging. $config['debug'] = false; - // For development purposes. Turns 'display_errors' on. Not recommended for production. + // For development purposes. Displays (and "dies" on) all errors and warnings. Turn on with the above. $config['verbose_errors'] = true; - - // Directory where temporary files will be created. Not really used much yet except for some experimental stuff. + + // Directory where temporary files will be created. $config['tmp'] = sys_get_temp_dir(); - + // The HTTP status code to use when redirecting. http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html // Can be either 303 "See Other" or 302 "Found". (303 is more correct but both should work.) + // There is really no reason for you to ever need to change this. $config['redirect_http'] = 303; - - // A small file in the main directory indicating that the script has been ran and the board(s) have been generated. - // This keeps the script from querying the database and causing strain when not needed. + + // A tiny text file in the main directory indicating that the script has been ran and the board(s) have + // been generated. This keeps the script from querying the database and causing strain when not needed. $config['has_installed'] = '.installed'; - + // Use syslog() for logging all error messages and unauthorized login attempts. $config['syslog'] = false; - + // Use `host` via shell_exec() to lookup hostnames, avoiding query timeouts. May not work on your system. // Requires safe_mode to be disabled. $config['dns_system'] = false; - + /* * ==================== * Database settings * ==================== */ - - // SQL driver ("mysql", "pgsql", "sqlite", "dblib", etc) - // http://www.php.net/manual/en/pdo.drivers.php + + // Database driver (http://www.php.net/manual/en/pdo.drivers.php) + // Only MySQL is supported by Tinyboard at the moment, sorry. $config['db']['type'] = 'mysql'; // Hostname or IP address $config['db']['server'] = 'localhost'; @@ -81,60 +85,72 @@ // Use a persistent connection (experimental) $config['db']['persistent'] = false; // Anything more to add to the DSN string (eg. port=xxx;foo=bar) - $config['db']['dsn'] = 'charset=UTF8'; + $config['db']['dsn'] = ''; // Timeout duration in seconds (not all drivers support this) $config['db']['timeout'] = 5; - + /* * ==================== * Cache settings * ==================== */ - - $config['cache']['enabled'] = false; - // $config['cache']['enabled'] = 'memcached'; - // $config['cache']['enabled'] = 'redis'; - // $config['cache']['enabled'] = 'apc'; - // $config['cache']['enabled'] = 'xcache'; - - // Timeout for cached objects such as posts and HTML - $config['cache']['timeout'] = 43200; // 12 hours - - // Optional prefix if you're running multiple Tinyboard instances on the same machine + + /* + * On top of the static file caching system, you can enable the additional caching system which is + * designed to minimize SQL queries and can significantly increase speed when posting or using the + * moderator interface. APC is the recommended method of caching. + * + * http://tinyboard.org/docs/index.php?p=Config/Cache + */ + + $config['cache']['enabled'] = false; + // $config['cache']['enabled'] = 'xcache'; + // $config['cache']['enabled'] = 'apc'; + // $config['cache']['enabled'] = 'memcached'; + // $config['cache']['enabled'] = 'redis'; + + // Timeout for cached objects such as posts and HTML. + $config['cache']['timeout'] = 60 * 60 * 48; // 48 hours + + // Optional prefix if you're running multiple Tinyboard instances on the same machine. $config['cache']['prefix'] = ''; - - // Memcached servers to use - http://www.php.net/manual/en/memcached.addservers.php + + // Memcached servers to use. Read more: http://www.php.net/manual/en/memcached.addservers.php $config['cache']['memcached'] = array( array('localhost', 11211) ); // Redis server to use. Location, port, password, database id. - // Note that Tinyboard may clear the database at times, so you may want to pick a - // database id just for Tinyboard to use. + // Note that Tinyboard may clear the database at times, so you may want to pick a database id just for + // Tinyboard to use. $config['cache']['redis'] = array('localhost', 6379, '', 1); - + /* * ==================== * Cookie settings * ==================== */ - - // Used for moderation login + + // Used for moderation login. $config['cookies']['mod'] = 'mod'; + // Used for communicating with Javascript; telling it when posts were successful. - // Rebuild Javascript file after changing this value or it won't work. $config['cookies']['js'] = 'serv'; - // Cookies "path". Defaults to $config['root']. If $config['root'] is a URL, you need to set this. Should be '/' or '/board/', depending on your installation. + + // Cookies path. Defaults to $config['root']. If $config['root'] is a URL, you need to set this. Should + // be '/' or '/board/', depending on your installation. // $config['cookies']['path'] = '/'; // Where to set the 'path' parameter to $config['cookies']['path'] when creating cookies. Recommended. $config['cookies']['jail'] = true; - // How long should the cookies last (in seconds) - $config['cookies']['expire'] = 15778463; //6 months - // Make this something long and random for security + + // How long should the cookies last (in seconds). Defines how long should moderators should remain logged + // in (0 = browser session). + $config['cookies']['expire'] = 60 * 60 * 24 * 30 * 6; // ~6 months + + // Make this something long and random for security. $config['cookies']['salt'] = 'abcdefghijklmnopqrstuvwxyz09123456789!@#$%^&*()'; - // How long should moderators should remain logged in (0=browser session) (in seconds) - $config['mod']['expire'] = 15778463; //6 months - // Used to salt secure tripcodes (##trip) and poster IDs (if enabled) + + // Used to salt secure tripcodes ("##trip") and poster IDs (if enabled). $config['secure_trip_salt'] = ')(*&^%$#@!98765432190zyxwvutsrqponmlkjihgfedcba'; /* @@ -142,22 +158,29 @@ * Flood/spam settings * ==================== */ - - // How many seconds between each post + + // Minimum time between between each post by the same IP address. $config['flood_time'] = 10; - // How many seconds between each post with exactly the same content and same IP + // Minimum time between between each post with the exact same content AND same IP address. $config['flood_time_ip'] = 120; - // Same as above but different IP address + // Same as above but by a different IP address. (Same content, not necessarily same IP address.) $config['flood_time_same'] = 30; - - // DNS blacklists (DNSBL) http://tinyboard.org/docs/?p=Config/DNSBL - - // http://www.sectoor.de/tor.php - $config['dnsbl'][] = array('tor.dnsbl.sectoor.de', 1); // Tor exit servers - + + /* + * To further prevent spam and abuse, you can use DNS blacklists (DNSBL). A DNSBL is a list of IP + * addresses published through the Internet Domain Name Service (DNS) either as a zone file that can be + * used by DNS server software, or as a live DNS zone that can be queried in real-time. + * + * Read more: http://tinyboard.org/docs/?p=Config/DNSBL + */ + + // Prevents most Tor exit nodes from making posts. Recommended, as a lot of abuse comes from Tor because + // of the strong anonymity associated with it. + $config['dnsbl'][] = array('tor.dnsbl.sectoor.de', 1); + // http://www.sorbs.net/using.shtml // $config['dnsbl'][] = array('dnsbl.sorbs.net', array(2, 3, 4, 5, 6, 7, 8, 9)); - + // http://www.projecthoneypot.org/httpbl.php // $config['dnsbl'][] = array('<your access key>.%.dnsbl.httpbl.org', function($ip) { // $octets = explode('.', $ip); @@ -172,11 +195,10 @@ // // return true; // }, 'dnsbl.httpbl.org'); // hide our access key - - + // Skip checking certain IP addresses against blacklists (for troubleshooting or whatever) $config['dnsbl_exceptions'][] = '127.0.0.1'; - + /* * Introduction to Tinyboard's spam filter: * @@ -196,14 +218,17 @@ * See also: http://tinyboard.org/docs/?p=Your_request_looks_automated * */ - - // Number of hidden fields to generate + + // Number of hidden fields to generate. $config['spam']['hidden_inputs_min'] = 4; $config['spam']['hidden_inputs_max'] = 12; + // How many times can a "hash" be used to post? $config['spam']['hidden_inputs_max_pass'] = 12; + // How soon after regeneration do hashes expire (in seconds)? $config['spam']['hidden_inputs_expire'] = 60 * 60 * 3; // three hours + // These are fields used to confuse the bots. Make sure they aren't actually used by Tinyboard, or it won't work. $config['spam']['hidden_input_names'] = array( 'user', @@ -217,6 +242,7 @@ 'text', 'message' ); + // Always update this when adding new valid fields to the post form, or EVERYTHING WILL BE DETECTED AS SPAM! $config['spam']['valid_inputs'] = array( 'hash', @@ -236,40 +262,17 @@ 'recaptcha_challenge_field', 'recaptcha_response_field', 'spoiler', - 'quick-reply' + 'quick-reply', + 'page' ); - - // Custom flood filters. Detect flood attacks and reject new posts if there's a positive match. - // See http://tinyboard.org/wiki/index.php?title=Flood_filters for more information. - //$config['flood_filters'][] = array( - // 'condition' => array( - // // 100 posts in the past 5 minutes (~20 p/m) - // 'posts_in_past_x_minutes' => array(100, 5) - // ), - // // Don't allow the user to post - // 'action' => 'reject', - // // Display this message - // 'message' => 'Your post has been rejected on the suspicion of a flood attack on this board.' - //); - - // Another filter - //$config['flood_filters'][] = array( - // 'condition' => array( - // // 10 new empty threads in the past 2 minutes - // 'threads_with_no_replies_in_past_x_minutes' => array(10, 2), - // // Allow replies, but not new threads (ie. reject topics only). - // 'OP' => true - // ), - // 'action' => 'reject', - // 'message' => 'Your post has been rejected on the suspicion of a flood attack on this board (too many new threads); post a reply instead.' - //); - - // Enable reCaptcha to make spam even harder + + // Enable reCaptcha to make spam even harder. Rarely necessary. $config['recaptcha'] = false; + // Public and private key pair from https://www.google.com/recaptcha/admin/create $config['recaptcha_public'] = '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f'; $config['recaptcha_private'] = '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_'; - + /* * ==================== * Post settings @@ -280,107 +283,156 @@ $config['force_body'] = false; // Do you need a body for new threads? $config['force_body_op'] = true; - // Strip superfluous new lines at the end of a post - $config['strip_superfluous_returns'] = true; // Require an image for threads? $config['force_image_op'] = true; - // Strip combining characters from Unicode strings (eg. "Zalgo") + + // Strip superfluous new lines at the end of a post. + $config['strip_superfluous_returns'] = true; + // Strip combining characters from Unicode strings (eg. "Zalgo"). $config['strip_combining_chars'] = true; - - // Max body length + + // Maximum post body length. $config['max_body'] = 1800; - // Amount of post lines to show on the index page + // Maximum number of post body lines to show on the index page. $config['body_truncate'] = 15; - // Amount of characters to show on the index page + // Maximum number of characters to show on the index page. $config['body_truncate_char'] = 2500; - - // Typically spambots try to post a lot of links. Refuse a post with X standalone links? + + // Typically spambots try to post many links. Refuse a post with X links? $config['max_links'] = 20; - // Maximum number of cites per post (protects against abuse) + // Maximum number of cites per post (prevents abuse, as more citations mean more database queries). $config['max_cites'] = 45; - // Maximum number of cross-board links/cites per post + // Maximum number of cross-board links/citations per post. $config['max_cross'] = $config['max_cites']; - + // Track post citations (>>XX). Rebuilds posts after a cited post is deleted, removing broken links. - // A little more database load. + // Puts a little more load on the database. $config['track_cites'] = true; - - // Maximum filename length (will be truncated) + + // Maximum filename length (will be truncated). $config['max_filename_len'] = 255; - // Maximum filename length to display (the rest can be viewed upon mouseover) + // Maximum filename length to display (the rest can be viewed upon mouseover). $config['max_filename_display'] = 30; - - // How long before you can delete a post after posting, in seconds. + + // How long after posting should you have to wait before being able to delete that post? (In seconds.) $config['delete_time'] = 10; - // Reply limit (stops bumping thread when this is reached) + // Reply limit (stops bumping thread when this is reached). $config['reply_limit'] = 250; - - // Image hard limit (stops allowing new image replies when this is reached if not zero) + + // Image hard limit (stops allowing new image replies when this is reached if not zero). $config['image_hard_limit'] = 0; - // Reply hard limit (stops allowing new replies when this is reached if not zero) + // Reply hard limit (stops allowing new replies when this is reached if not zero). $config['reply_hard_limit'] = 0; - - // Strip repeating characters when making hashes + + $config['robot_enable'] = false; + // Strip repeating characters when making hashes. $config['robot_strip_repeating'] = true; - - // Enable mutes - // Tinyboard uses ROBOT9000's original 2^x implementation + // Enabled mutes? Tinyboard uses ROBOT9000's original 2^x implementation where x is the number of times + // you have been muted in the past. $config['robot_mute'] = true; - // How many mutes x hours ago to include in the algorithm + // How long before Tinyboard forgets about a mute? $config['robot_mute_hour'] = 336; // 2 weeks - // If you want to alter the algorithm a bit. Default value is 2. n^x - $config['robot_mute_multiplier'] = 2; + // If you want to alter the algorithm a bit. Default value is 2. + $config['robot_mute_multiplier'] = 2; // (n^x where x is the number of previous mutes) $config['robot_mute_descritpion'] = _('You have been muted for unoriginal content.'); - - // Automatically convert things like "..." to Unicode characters ("…") + + // Automatically convert things like "..." to Unicode characters ("…"). $config['auto_unicode'] = true; - // Whether to turn URLs into functional links + // Whether to turn URLs into functional links. $config['markup_urls'] = true; - // Prefix those functional links with some other url, for example some service for hiding referers - // (eg. 'http://www.nullrefer.com/?', or 'http://anonym.to/?'), a pay-per-view exit page, etc. + // Optional URL prefix for links (eg. "http://anonym.to/?"). + $config['link_prefix'] = ''; $config['url_ads'] = &$config['link_prefix']; // leave alias - // Optional URL prefix for links (eg. "http://anonym.to/?") - $config['link_prefix'] = ''; - - - // Wordfilters are used to automatically replace certain words/phrases with something else. + // 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. + // For a normal string replacement: - // $config['wordfilters'][] = array('cat', 'dog'); - + // $config['wordfilters'][] = array('cat', 'dog'); // Advanced raplcement (regular expressions): - // $config['wordfilters'][] = array('/cat/', 'dog', true); // 'true' means it's a regular expression - - // Always act as if they had typed "noko" in the email field no mattter what + // $config['wordfilters'][] = array('/ca[rt]/', 'dog', true); // 'true' means it's a regular expression + + // Always act as if the user had typed "noko" into the email field. $config['always_noko'] = false; - - // Custom tripcodes. The below example makes a tripcode - // of "#test123" evaluate to "!HelloWorld" + + // Example: Custom tripcodes. The below example makes a tripcode of "#test123" evaluate to "!HelloWorld". // $config['custom_tripcode']['#test123'] = '!HelloWorld'; + // Example: Custom secure tripcode. // $config['custom_tripcode']['##securetrip'] = '!!somethingelse'; - - // Optional spoiler images + + // Allow users to mark their image as a "spoiler" when posting. The thumbnail will be replaced with a + // static spoiler image instead (see $config['spoiler_image']). $config['spoiler_images'] = false; - - + // With the following, you can disable certain superfluous fields or enable "forced anonymous". - + // When true, all names will be set to $config['anonymous']. $config['field_disable_name'] = false; - // When true, no email will be able to be set. + // When true, there will be no email field. $config['field_disable_email'] = false; - // When true, no subject will be able to be set. + // When true, there will be no subject field. $config['field_disable_subject'] = false; - // When true, no subject will be able to be set in replies. + // When true, there will be no subject field for replies. $config['field_disable_reply_subject'] = false; // When true, a blank password will be used for files (not usable for deletion). $config['field_disable_password'] = false; - - // Require users to see the ban page at least once for a ban even if it has since expired? - $config['require_ban_view'] = false; - + + // Require users to see the ban page at least once for a ban even if it has since expired. + $config['require_ban_view'] = true; + + /* + * Custom filters detect certain posts and reject/ban accordingly. They are made up of a + * condition and an action (for when ALL conditions are met). As every single post has to + * be put through each filter, having hundreds probably isn’t ideal as it could slow things down. + * + * Read more: http://tinyboard.org/docs/index.php?p=Config/Filters + * + * This used to be named $config['flood_filters'] (still exists as an alias). + */ + + // An example of blocking an imaginary known spammer, who keeps posting a reply with the name "surgeon", + // ending his posts with "regards, the surgeon" or similar. + // $config['filters'][] = array( + // 'condition' => array( + // 'name' => '/^surgeon$/', + // 'body' => '/regards,\s+(the )?surgeon$/i', + // 'OP' => false + // ), + // 'action' => 'reject', + // 'message' => 'Go away, spammer.' + // ); + + // Same as above, but issuing a 3-hour ban instead of just reject the post. + // $config['filters'][] = array( + // 'condition' => array( + // 'name' => '/^surgeon$/', + // 'body' => '/regards,\s+(the )?surgeon$/i', + // 'OP' => false + // ), + // 'action' => 'ban', + // 'expires' => 60 * 60 * 3, // 3 hours + // 'reason' => 'Go away, spammer.' + // ); + + // PHP 5.3+ (anonymous functions) + // There is also a "custom" condition, making the possibilities of this feature pretty much endless. + // This is a bad example, because there is already a "name" condition built-in. + // $config['filters'][] = array( + // 'condition' => array( + // 'body' => '/h$/i', + // 'OP' => false, + // 'custom' => function($post) { + // if($post['name'] == 'Anonymous') + // return true; + // else + // return false; + // } + // ), + // 'action' => 'reject' + // ); + /* * ==================== * Markup settings @@ -391,122 +443,137 @@ $config['markup'][] = array("/'''([^<]+?)'''/", "<strong>\$1</strong>"); $config['markup'][] = array("/''([^<]+?)''/", "<em>\$1</em>"); $config['markup'][] = array("/\*\*([^<]+?)\*\*/", "<span class=\"spoiler\">\$1</span>"); - $config['markup'][] = array("/^[ |\t]*==([^<]+?)==[ |\t]*$/m", "<span class=\"heading\">\$1</span>"); - - // Highlight PHP code wrapped in <code> tags (PHP 5.3.0+) + // $config['markup'][] = array("/^[ |\t]*==([^<]+?)==[ |\t]*$/m", "<span class=\"heading\">\$1</span>"); + + // Highlight PHP code wrapped in <code> tags (PHP 5.3+) // $config['markup'][] = array( // '/^<code>(.+)<\/code>/ms', // function($matches) { // return highlight_string(html_entity_decode($matches[1]), true); // } // ); - + /* * ==================== * Image settings * ==================== */ - - // For resizing, max thumbnail size + + // For resizing, maximum thumbnail dimensions. $config['thumb_width'] = 255; $config['thumb_height'] = 255; - // Max thumbnail size for thread images + // Maximum thumbnail dimensions for thread (OP) images. $config['thumb_op_width'] = 255; $config['thumb_op_height'] = 255; - - // Thumbnail extension, empty for inherited (png recommended) + + // Thumbnail extension ("png" recommended). Leave this empty if you want the extension to be inherited + // from the uploaded file. $config['thumb_ext'] = 'png'; - - // Maximum amount of frames to resize (more frames means more processing power). "1" means no animated thumbnails. - // Requires $config['thumb_ext'] to be 'gif' and $config['thumb_method'] to be 'imagick', 'convert', or 'convert+gifsicle'. - // Not respected by 'convert'; will just resize all frames if this is > 1. + + // Maximum amount of animated GIF frames to resize (more frames can mean more processing power). A value + // of "1" means thumbnails will not be animated. Requires $config['thumb_ext'] to be 'gif' (or blank) and + // $config['thumb_method'] to be 'imagick', 'convert', or 'convert+gifsicle'. This value is not respected + // by 'convert'; will just resize all frames if this is > 1. $config['thumb_keep_animation_frames'] = 1; - - // Thumbnailing method: - // - 'gd' PHP GD (default). Only handles the most basic image formats (GIF, JPEG, PNG). - // This is a prerequisite for Tinyboard no matter what method you choose. - // - 'imagick' PHP's ImageMagick bindings. Fast and efficient, supporting many image formats. - // A few minor bugs. http://pecl.php.net/package/imagick - // - 'convert' The command line version of ImageMagick (`convert`). Fixes most of the bugs in - // PHP Imagick. `convert` produces the best still thumbnails and is highly recommended. - // - 'convert+gifsicle' Same as above, with the exception of using `gifsicle` (command line application) - // instead of `convert` for resizing GIFs. It's faster and resulting animated thumbnails - // have less artifacts than if resized with ImageMagick. + + /* + * Thumbnailing method: + * + * 'gd' PHP GD (default). Only handles the most basic image formats (GIF, JPEG, PNG). + * GD is a prerequisite for Tinyboard no matter what method you choose. + * + * 'imagick' PHP's ImageMagick bindings. Fast and efficient, supporting many image formats. + * A few minor bugs. http://pecl.php.net/package/imagick + * + * 'convert' The command line version of ImageMagick (`convert`). Fixes most of the bugs in + * PHP Imagick. `convert` produces the best still thumbnails and is highly recommended. + * + * 'convert+gifsicle' Same as above, with the exception of using `gifsicle` (command line application) + * instead of `convert` for resizing GIFs. It's faster and resulting animated + * thumbnails have less artifacts than if resized with ImageMagick. + */ $config['thumb_method'] = 'gd'; // $config['thumb_method'] = 'convert'; - - // Strip EXIF metadata from JPEG files + + // Command-line options passed to ImageMagick when using `convert` for thumbnailing. Don't touch the + // placement of "%s" and "%d". + $config['convert_args'] = '-background transparent %s -strip -thumbnail %dx%d -quality 65'; + + // Strip EXIF metadata from JPEG files. $config['strip_exif'] = false; - - // Regular expression to check for IE MIME type detection XSS exploit. To disable, comment the line out - // https://github.com/savetheinternet/Tinyboard/issues/20 + + // Regular expression to check for an XSS exploit with IE 6 and 7. To disable, set to false. + // Details: https://github.com/savetheinternet/Tinyboard/issues/20 $config['ie_mime_type_detection'] = '/<(?:body|head|html|img|plaintext|pre|script|table|title|a href|channel|scriptlet)/i'; - - // Allowed image file extensions + + // Allowed image file extensions. $config['allowed_ext'][] = 'jpg'; $config['allowed_ext'][] = 'jpeg'; $config['allowed_ext'][] = 'bmp'; $config['allowed_ext'][] = 'gif'; $config['allowed_ext'][] = 'png'; // $config['allowed_ext'][] = 'svg'; - - // Allowed additional file extensions (not images; downloadable files) + + // Allowed additional file extensions (not images; downloadable files). // $config['allowed_ext_files'][] = 'txt'; // $config['allowed_ext_files'][] = 'zip'; - - // An alternative function for generating a filename, instead of the default UNIX timestamp. - // http://tinyboard.org/wiki/index.php?title=Filenames - // $config['filename_func'] = 'some_function_you_have_created'; - - // Non-image file icons + + // An alternative function for generating image filenames, instead of the default UNIX timestamp. + // $config['filename_func'] = function($post) { + // return sprintf("%s", time() . substr(microtime(), 2, 3)); + // }; + + // Thumbnail to use for the non-image file uploads. $config['file_icons']['default'] = 'file.png'; $config['file_icons']['zip'] = 'zip.png'; - - // Thumbnail to use for the downloadable files (not images) + // Example: Custom thumbnail for certain file extension. + // $config['file_icons']['extension'] = 'some_file.png'; + + // Location of above images. $config['file_thumb'] = 'static/%s'; - // Thumbnail to use for spoiler images + // Location of thumbnail to use for spoiler images. $config['spoiler_image'] = 'static/spoiler.png'; - - // Thumbnail quality (compression level), from 0 to 9 - $config['thumb_quality'] = 8; - - // 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 + // Location of thumbnail to use for deleted images. + $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. $config['minimum_copy_resize'] = false; - - // Store image hash in the database for r9k-like boards implementation soon - // Function name for hashing + + // Image hashing function. There's really no reason to change this. // sha1_file, md5_file, etc. You can also define your own similar function. $config['file_hash'] = 'sha1_file'; - - // Maximum image upload size in bytes - $config['max_filesize'] = 10*1024*1024; // 10MB - // Maximum image dimensions + + // Maximum image upload size in bytes. + $config['max_filesize'] = 10 * 1024 * 1024; // 10MB + // Maximum image dimensions. $config['max_width'] = 10000; - $config['max_height'] = $config['max_width']; // 1:1 - // Reject duplicate image uploads + $config['max_height'] = $config['max_width']; + // Reject duplicate image uploads. $config['image_reject_repost'] = true; - // Reject duplicate image uploads within the same thread. Doesn't change anything if image_reject_repost is true. + // Reject duplicate image uploads within the same thread. Doesn't change anything if + // $config['image_reject_repost'] is true. $config['image_reject_repost_in_thread'] = false; - - // Display the aspect ratio in a post's file info + + // Display the aspect ratio of uploaded files. $config['show_ratio'] = false; - // Display the file's original filename + // Display the file's original filename. $config['show_filename']= true; - // Image identification links using regex.info/exif, TinEye and Google Images + // Display image identification links using regex.info/exif, TinEye and Google Images. $config['image_identification'] = false; - - // Redraw the image using GD functions to strip any excess data (commonly ZIP archives) - // WARNING: Currently strips animated GIFs too + + // Redraw the image to strip any excess data (commonly ZIP archives) WARNING: This might strip the + // animation of GIFs, depending on the chosen thumbnailing method. $config['redraw_image'] = false; - + /* * ==================== * Board settings * ==================== */ - // Maximum amount of threads to display on a given page. + // Maximum amount of threads to display per page. $config['threads_per_page'] = 10; // Maximum number of pages. Content past the last page is automatically purged. $config['max_pages'] = 10; @@ -515,14 +582,14 @@ // Same as above, but for stickied threads. $config['threads_preview_sticky'] = 1; - // Name of the boards. Usually '/%s/' (/b/, /mu/, etc) - // $config['board_abbreviation'] - BOARD_TITLE + // How to display the URI of boards. Usually '/%s/' (/b/, /mu/, etc). This doesn't change the URL. Find + // $config['board_path'] if you wish to change the URL. $config['board_abbreviation'] = '/%s/'; - - // The default name (ie. Anonymous) + + // The default name (ie. Anonymous). $config['anonymous'] = 'Anonymous'; - - // How many reports you can create in the same request. + + // Number of reports you can create at once. $config['report_limit'] = 3; // Attention Whoring Bar @@ -534,91 +601,96 @@ * Display settings * ==================== */ - - // Locale (en, ru_RU.UTF-8, fi_FI.UTF-8, pl_PL.UTF-8) - $config['locale'] = 'en'; - - // Timezone + + // Tinyboard has been translated into a few langauges. See inc/locale for available translations. + $config['locale'] = 'en'; // (en, ru_RU.UTF-8, fi_FI.UTF-8, pl_PL.UTF-8) + + // Timezone to use for displaying dates/tiems. $config['timezone'] = 'America/Los_Angeles'; - - // The format string passed to strftime() for post times + // The format string passed to strftime() for displaying dates. // http://www.php.net/manual/en/function.strftime.php $config['post_date'] = '%m/%d/%y (%a) %H:%M:%S'; - // Same as above, but used for "you are banned' pages. $config['ban_date'] = '%A %e %B, %Y'; - - // The names on the post buttons. (On most imageboards, these are both "Post") + + // The names on the post buttons. (On most imageboards, these are both just "Post"). $config['button_newtopic'] = _('New Topic'); $config['button_reply'] = _('New Reply'); - - // Assign each poster in a thread a unique ID, shown by "ID: {id}" before the post number. + + // Assign each poster in a thread a unique ID, shown by "ID: xxxxx" before the post number. $config['poster_ids'] = false; - // Number of characters in the poster ID (maximum is 40) + // Number of characters in the poster ID (maximum is 40). $config['poster_id_length'] = 5; - - // Show thread subject in page title? + + // Show thread subject in page title. $config['thread_subject_in_title'] = false; - - // Page footer + + // Additional lines added to the footer of all pages. $config['footer'][] = _('All trademarks, copyrights, comments, and images on this page are owned by and are the responsibility of their respective parties.'); - - // Characters used to generate a random password (with Javascript) + + // Characters used to generate a random password (with Javascript). $config['genpassword_chars'] = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+'; - - // Optional banner at the top of every page. + + // Optional banner image at the top of every page. // $config['url_banner'] = '/banner.php'; - // Banner dimensions are also optional. As the banner loads after the rest of the page, everything - // may be shifted down a few pixels when it does. Making the banner a fixed size will prevent this. + // Banner dimensions are also optional. As the banner loads after the rest of the page, everything may be + // shifted down a few pixels when it does. Making the banner a fixed size will prevent this. // $config['banner_width'] = 300; // $config['banner_height'] = 100; - - // Custom stylesheets available. The prefix for each stylesheet URI is defined below. - $config['stylesheets']['Yotsuba B'] = ''; // default + + // Custom stylesheets available for the user to choose. See the "stylesheets/" folder for a list of + // available stylesheets (or create your own). + $config['stylesheets']['Yotsuba B'] = ''; // Default; there is no additional/custom stylesheet for this. $config['stylesheets']['Yotsuba'] = 'yotsuba.css'; // $config['stylesheets']['Futaba'] = 'futaba.css'; - + // $config['stylesheets']['Dark'] = 'dark.css'; + // The prefix for each stylesheet URI. Defaults to $config['root']/stylesheets/ // $config['uri_stylesheets'] = 'http://static.example.org/stylesheets/'; - - // The default stylesheet to use + + // The default stylesheet to use. $config['default_stylesheet'] = array('Yotsuba B', $config['stylesheets']['Yotsuba B']); - - // Make stylesheet selections board-specific + + // Make stylesheet selections board-specific. $config['stylesheets_board'] = false; - + // Use Font-Awesome for displaying lock and pin icons, instead of the images in static/. // http://fortawesome.github.io/Font-Awesome/icon/pushpin/ // http://fortawesome.github.io/Font-Awesome/icon/lock/ $config['font_awesome'] = true; $config['font_awesome_css'] = 'stylesheets/font-awesome/css/font-awesome.min.css'; - - // Boardlinks - // You can group, order and place the boardlist at the top of every page, using the following template. - //$config['boards'] = array( - // array('a', 'b'), - // array('c', 'd', 'e', 'f', 'g'), - // array('h', 'i', 'j'), - // array('k', array('l', 'm')), - // array('status' => 'http://status.example.org/') - //); - - // Categories - // Required for the Categories theme. - //$config['categories'] = array( - // 'Group Name' => array('a', 'b', 'c'), - // 'Another Group' => array('d') - //); - - // Custom_categories - // Optional for the Categories theme. array of name => (title, url) groups for categories with non-board links. - //$config['custom_categories'] = array( - // 'Links' => array( - // 'Tinyboard' => 'http://tinyboard.org', - // 'Donate' => 'donate.html' - // ) - //); - + + /* + * For lack of a better name, “boardlinks” are those sets of navigational links that appear at the top + * and bottom of board pages. They can be a list of links to boards and/or other pages such as status + * blogs and social network profiles/pages. + * + * "Groups" in the boardlinks are marked with square brackets. Tinyboard allows for infinite recursion + * with groups. Each array() in $config['boards'] represents a new square bracket group. + */ + + // $config['boards'] = array( + // array('a', 'b'), + // array('c', 'd', 'e', 'f', 'g'), + // array('h', 'i', 'j'), + // array('k', array('l', 'm')), + // array('status' => 'http://status.example.org/') + // ); + + // Board categories. Only used in the "Categories" theme. + // $config['categories'] = array( + // 'Group Name' => array('a', 'b', 'c'), + // 'Another Group' => array('d') + // ); + // Optional for the Categories theme. This is an array of name => (title, url) groups for categories + // with non-board links. + // $config['custom_categories'] = array( + // 'Links' => array( + // 'Tinyboard' => 'http://tinyboard.org', + // 'Donate' => 'donate.html' + // ) + // ); + // Automatically remove unnecessary whitespace when compiling HTML files from templates. $config['minify_html'] = true; @@ -628,25 +700,31 @@ * ==================== */ - // Additional Javascript files to include on board index and thread pages. + // Additional Javascript files to include on board index and thread pages. See js/ for available scripts. $config['additional_javascript'][] = 'js/inline-expanding.js'; // $config['additional_javascript'][] = 'js/local-time.js'; - - // Some scripts require jQuery. Check the comments in script files to see what's needed. + + // Some scripts require jQuery. Check the comments in script files to see what's needed. When enabling + // jQuery, you should first empty the array so that "js/query.min.js" can be the first, and then re-add + // "js/inline-expanding.js" or else the inline-expanding script might not interact properly with other + // scripts. + // $config['additional_javascript'] = array(); // $config['additional_javascript'][] = 'js/jquery.min.js'; + // $config['additional_javascript'][] = 'js/inline-expanding.js'; // $config['additional_javascript'][] = 'js/auto-reload.js'; + // $config['additional_javascript'][] = 'js/post-hover.js'; + // $config['additional_javascript'][] = 'js/style-select.js'; + + // Where these script files are located on the web. Defaults to $config['root']. + // $config['additional_javascript_url'] = 'http://static.example.org/tinyboard-javascript-stuff/'; - // Where these script files are located on the web (defaults to $config['root']). - // $config['additional_javascript_url'] = '/js/'; - // Compile all additional scripts into one file ($config['file_script']) instead of including them seperately. $config['additional_javascript_compile'] = false; - - // Minify Javascript using http://code.google.com/p/minify/ + + // Minify Javascript using http://code.google.com/p/minify/. $config['minify_js'] = false; - // Allows js/quick-reply.js to work - // This will make your imageboard more vulnerable to flood attacks. + // Allows js/quick-reply.js to work. This could make your imageboard more vulnerable to flood attacks. $config['quick_reply'] = false; /* @@ -654,12 +732,12 @@ * Video embedding * ==================== */ - - // Enable embedding (see below) + + // Enable embedding (see below). $config['enable_embedding'] = false; - + // Custom embedding (YouTube, vimeo, etc.) - // It's very important that you match the full string (with ^ and $) or things will not work correctly. + // It's very important that you match the entire input (with ^ and $) or things will not work correctly. $config['embedding'] = array( array( '/^https?:\/\/(\w+\.)?youtube\.com\/watch\?v=([a-zA-Z0-9\-_]{10,11})(&.+)?$/i', @@ -686,39 +764,39 @@ '<object style="float: left;margin: 10px 20px;" width="148" height="44"><param name="movie" value="http://vocaroo.com/player.swf?playMediaID=$2&autoplay=0"></param><param name="wmode" value="transparent"></param><embed src="http://vocaroo.com/player.swf?playMediaID=$2&autoplay=0" width="148" height="44" wmode="transparent" type="application/x-shockwave-flash"></embed></object>' ) ); - - // Embedding width and height + + // Embedding width and height. $config['embed_width'] = 300; $config['embed_height'] = 246; - + /* * ==================== * Error messages * ==================== */ - + // Error messages - $config['error']['lurk'] = _('Lurk some more before posting.'); - $config['error']['bot'] = _('You look like a bot.'); - $config['error']['referer'] = _('Your browser sent an invalid or no HTTP referer.'); - $config['error']['toolong'] = _('The %s field was too long.'); + $config['error']['lurk'] = _('Lurk some more before posting.'); + $config['error']['bot'] = _('You look like a bot.'); + $config['error']['referer'] = _('Your browser sent an invalid or no HTTP referer.'); + $config['error']['toolong'] = _('The %s field was too long.'); $config['error']['toolong_body'] = _('The body was too long.'); $config['error']['tooshort_body'] = _('The body was too short or empty.'); - $config['error']['noimage'] = _('You must upload an image.'); - $config['error']['nomove'] = _('The server failed to handle your upload.'); - $config['error']['fileext'] = _('Unsupported image format.'); - $config['error']['noboard'] = _('Invalid board!'); + $config['error']['noimage'] = _('You must upload an image.'); + $config['error']['nomove'] = _('The server failed to handle your upload.'); + $config['error']['fileext'] = _('Unsupported image format.'); + $config['error']['noboard'] = _('Invalid board!'); $config['error']['nonexistant'] = _('Thread specified does not exist.'); - $config['error']['locked'] = _('Thread locked. You may not reply at this time.'); + $config['error']['locked'] = _('Thread locked. You may not reply at this time.'); $config['error']['reply_hard_limit'] = _('Thread has reached its maximum reply limit.'); $config['error']['image_hard_limit'] = _('Thread has reached its maximum image limit.'); - $config['error']['nopost'] = _('You didn\'t make a post.'); - $config['error']['flood'] = _('Flood detected; Post discarded.'); - $config['error']['spam'] = _('Your request looks automated; Post discarded.'); + $config['error']['nopost'] = _('You didn\'t make a post.'); + $config['error']['flood'] = _('Flood detected; Post discarded.'); + $config['error']['spam'] = _('Your request looks automated; Post discarded.'); $config['error']['unoriginal'] = _('Unoriginal content!'); - $config['error']['muted'] = _('Unoriginal content! You have been muted for %d seconds.'); + $config['error']['muted'] = _('Unoriginal content! You have been muted for %d seconds.'); $config['error']['youaremuted'] = _('You are muted! Expires in %d seconds.'); - $config['error']['dnsbl'] = _('Your IP address is listed in %s.'); + $config['error']['dnsbl'] = _('Your IP address is listed in %s.'); $config['error']['toomanylinks'] = _('Too many links; flood detected.'); $config['error']['toomanycites'] = _('Too many cites; post discarded.'); $config['error']['toomanycross'] = _('Too many cross-board links; post discarded.'); @@ -729,19 +807,19 @@ $config['error']['invalidimg'] = _('Invalid image.'); $config['error']['unknownext'] = _('Unknown file extension.'); $config['error']['filesize'] = _('Maximum file size: %maxsz% bytes<br>Your file\'s size: %filesz% bytes'); - $config['error']['maxsize'] = _('The file was too big.'); + $config['error']['maxsize'] = _('The file was too big.'); $config['error']['invalidzip'] = _('Invalid archive!'); $config['error']['fileexists'] = _('That file <a href="%s">already exists</a>!'); $config['error']['fileexistsinthread'] = _('That file <a href="%s">already exists</a> in this thread!'); $config['error']['delete_too_soon'] = _('You\'ll have to wait another %s before deleting that.'); $config['error']['mime_exploit'] = _('MIME type detection XSS exploit (IE) detected; post discarded.'); $config['error']['invalid_embed'] = _('Couldn\'t make sense of the URL of the video you tried to embed.'); - $config['error']['captcha'] = _('You seem to have mistyped the verification.'); - + $config['error']['captcha'] = _('You seem to have mistyped the verification.'); + // Moderator errors $config['error']['toomanyunban'] = _('You are only allowed to unban %s users at a time. You tried to unban %u users.'); - $config['error']['invalid'] = _('Invalid username and/or password.'); - $config['error']['notamod'] = _('You are not a mod…'); + $config['error']['invalid'] = _('Invalid username and/or password.'); + $config['error']['notamod'] = _('You are not a mod…'); $config['error']['invalidafter'] = _('Invalid username and/or password. Your user may have been deleted or changed.'); $config['error']['malformed'] = _('Invalid/malformed cookies.'); $config['error']['missedafield'] = _('Your browser didn\'t submit an input when it should have.'); @@ -750,98 +828,99 @@ $config['error']['boardexists'] = _('There is already a %s board.'); $config['error']['noaccess'] = _('You don\'t have permission to do that.'); $config['error']['invalidpost'] = _('That post doesn\'t exist…'); - $config['error']['404'] = _('Page not found.'); + $config['error']['404'] = _('Page not found.'); $config['error']['modexists'] = _('That mod <a href="?/users/%d">already exists</a>!'); $config['error']['invalidtheme'] = _('That theme doesn\'t exist!'); - $config['error']['csrf'] = _('Invalid security token! Please go back and try again.'); + $config['error']['csrf'] = _('Invalid security token! Please go back and try again.'); /* * ========================= * Directory/file settings * ========================= */ - + // The root directory, including the trailing slash, for Tinyboard. - // examples: '/', 'http://boards.chan.org/', '/chan/' + // Examples: '/', 'http://boards.chan.org/', '/chan/'. if (isset($_SERVER['REQUEST_URI'])) - $config['root'] = (str_replace('\\', '/', dirname($_SERVER['REQUEST_URI'])) == '/' ? '/' : str_replace('\\', '/', dirname($_SERVER['REQUEST_URI'])) . '/'); + $config['root'] = str_replace('\\', '/', dirname($_SERVER['REQUEST_URI'])) == '/' ? '/' : str_replace('\\', '/', dirname($_SERVER['REQUEST_URI'])) . '/'; else $config['root'] = '/'; // CLI mode // The scheme and domain. This is used to get the site's absolute URL (eg. for image identification links). // If you use the CLI tools, it would be wise to override this setting. - $config['domain'] = (isset ($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') ? "https://" : "http://"; - $config['domain'] .= isset ($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost'; - + $config['domain'] = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] != 'off') ? 'https://' : 'http://'; + $config['domain'] .= isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'localhost'; + // If for some reason the folders and static HTML index files aren't in the current working direcotry, // enter the directory path here. Otherwise, keep it false. $config['root_file'] = false; - + + // Location of files. $config['file_index'] = 'index.html'; $config['file_page'] = '%d.html'; $config['file_mod'] = 'mod.php'; $config['file_post'] = 'post.php'; $config['file_script'] = 'main.js'; - - // Board directory, followed by a forward-slash (/). (%s is board abbreviation) + + // Board directory, followed by a forward-slash (/). $config['board_path'] = '%s/'; - + // Misc directories. $config['dir']['img'] = 'src/'; $config['dir']['thumb'] = 'thumb/'; $config['dir']['res'] = 'res/'; - // For load balancing, having a seperate server (and domain/subdomain) for serving static content is possible. - // This can either be a directory or a URL (eg. http://static.example.org/) - //$config['dir']['static'] = $config['root'] . 'static/'; - // Where to store the .html templates. This folder and templates must exist or fatal errors will be thrown. + + // For load balancing, having a seperate server (and domain/subdomain) for serving static content is + // possible. This can either be a directory or a URL. Defaults to $config['root'] . 'static/'. + // $config['dir']['static'] = 'http://static.example.org/'; + + // Where to store the .html templates. This folder and the template files must exist. $config['dir']['template'] = getcwd() . '/templates'; - // For the themes (homepages, etc.) + // Location of Tinyboard "themes". $config['dir']['themes'] = getcwd() . '/templates/themes'; - // Same as above, but a URI (accessable by web interface, not locally) + // Same as above, but a URI (accessable by web interface). $config['dir']['themes_uri'] = 'templates/themes'; - // Homepage directory + // Home directory. Used by themes. $config['dir']['home'] = ''; - - // Static images - // These can be URLs OR base64 (data URI scheme) - //$config['image_sticky'] = $config['dir']['static'] . 'sticky.gif'; - //$config['image_locked'] = $config['dir']['static'] . 'locked.gif'; - //$config['image_bumplocked'] = $config['dir']['static'] . 'sage.gif'; - //$config['image_deleted'] = $config['dir']['static'] . 'deleted.'; - //$config['image_zip'] = $config['dir']['static'] . 'zip.'; - - // If you want to put images and other dynamic-static stuff on another (preferably cookieless) domain, you can use this: - // This will override $config['root'] and $config['dir']['...'] directives. - // "%s" will get replaced with $board['dir'], which usually includes a trailing slash. To avoid double slashes, you don't need - // to put a slash after %s + + // Static images. These can be URLs OR base64 (data URI scheme). These are only used if + // $config['font_awesome'] is false (default). + // $config['image_sticky'] = 'static/sticky.gif'; + // $config['image_locked'] = 'static/locked.gif'; + // $config['image_bumplocked'] = 'static/sage.gif'. + + // If you want to put images and other dynamic-static stuff on another (preferably cookieless) domain. + // This will override $config['root'] and $config['dir']['...'] directives. "%s" will get replaced with + // $board['dir'], which includes a trailing slash. // $config['uri_thumb'] = 'http://images.example.org/%sthumb/'; // $config['uri_img'] = 'http://images.example.org/%ssrc/'; - - // Set custom locations for stylesheets, scripts and maybe a banner. - // This can be good for load balancing across multiple servers or hostnames. + + // Set custom locations for stylesheets and the main script file. This can be used for load balancing + // across multiple servers or hostnames. // $config['url_stylesheet'] = 'http://static.example.org/style.css'; // main/base stylesheet // $config['url_javascript'] = 'http://static.example.org/main.js'; + + // Website favicon. // $config['url_favicon'] = '/favicon.gif'; + // EXPERIMENTAL: Try not to build pages when we shouldn't have to. + $config['try_smarter'] = false; + /* * ==================== * Mod settings * ==================== */ - - // Limit how many bans can be removed via the ban list. (Set too -1 to remove limit.) - $config['mod']['unban_limit'] = 5; - - // Whether or not to lock moderator sessions to the IP address that was logged in with. + + // Limit how many bans can be removed via the ban list. Set to -1 for no limit. + $config['mod']['unban_limit'] = -1; + + // Whether or not to lock moderator sessions to IP addresses. This makes cookie theft ineffective. $config['mod']['lock_ip'] = true; - - // The page that is first shown when a moderator logs in. Defaults to the dashboard. + + // The page that is first shown when a moderator logs in. Defaults to the dashboard (?/). $config['mod']['default'] = '/'; - - // Don't even display MySQL password to administrators (in the configuration page). - $config['mod']['never_reveal_password'] = true; - - // Mod links (full HTML) - // Correspond to above permission directives + + // Mod links (full HTML). $config['mod']['link_delete'] = '[D]'; $config['mod']['link_ban'] = '[B]'; $config['mod']['link_bandelete'] = '[B&D]'; @@ -856,92 +935,94 @@ $config['mod']['link_bumpunlock'] = '[-Sage]'; $config['mod']['link_editpost'] = '[Edit]'; $config['mod']['link_move'] = '[Move]'; - - // Moderator capcodes + + // Moderator capcodes. $config['capcode'] = ' <span class="capcode">## %s</span>'; - - // Custom capcodes, by example: - // "## Custom" becomes lightgreen, italic and bold + + // "## Custom" becomes lightgreen, italic and bold: //$config['custom_capcode']['Custom'] ='<span class="capcode" style="color:lightgreen;font-style:italic;font-weight:bold"> ## %s</span>'; - - // "## Mod" makes everything purple, including the name and tripcode + + // "## Mod" makes everything purple, including the name and tripcode: //$config['custom_capcode']['Mod'] = array( // '<span class="capcode" style="color:purple"> ## %s</span>', // 'color:purple', // Change name style; optional // 'color:purple' // Change tripcode style; optional //); - - // "## Admin" makes everything red and bold, including the name and tripcode + + // "## Admin" makes everything red and bold, including the name and tripcode: //$config['custom_capcode']['Admin'] = array( // '<span class="capcode" style="color:red;font-weight:bold"> ## %s</span>', // 'color:red;font-weight:bold', // Change name style; optional // 'color:red;font-weight:bold' // Change tripcode style; optional //); - - // Enable IP range bans (eg. "127.*.0.1", "127.0.0.*", and "12*.0.0.1" all match "127.0.0.1"). - // A little more load on the database + + // Enable IP range bans (eg. "127.*.0.1", "127.0.0.*", and "12*.0.0.1" all match "127.0.0.1"). Puts a + // little more load on the database $config['ban_range'] = true; - - // Enable CDIR netmask bans (eg. "10.0.0.0/8" for 10.0.0.0.0 - 10.255.255.255). Useful for stopping persistent spammers. - // Again, a little more database load. + + // Enable CDIR netmask bans (eg. "10.0.0.0/8" for 10.0.0.0.0 - 10.255.255.255). Useful for stopping + // persistent spammers and ban evaders. Again, a little more database load. $config['ban_cidr'] = true; - - // Do a DNS lookup on IP addresses to get their hostname on the IP summary page + + // Do DNS lookups on IP addresses to get their hostname for the moderator IP pages (?/IP/x.x.x.x). $config['mod']['dns_lookup'] = true; - // How many recent posts, per board, to show in the IP summary page + // How many recent posts, per board, to show in ?/IP/x.x.x.x. $config['mod']['ip_recentposts'] = 5; - - // How many posts to display on the reports page + + // Number of posts to display on the reports page. $config['mod']['recent_reports'] = 10; - - // How many actions to show per page in the moderation log + // Number of actions to show per page in the moderation log. $config['mod']['modlog_page'] = 350; - // How many bans to show per page in the ban list + // Number of bans to show per page in the ban list. $config['mod']['banlist_page'] = 350; - - // Number of news entries to display per page + // Number of news entries to display per page. $config['mod']['news_page'] = 40; - - // Number of results to display per page + // Number of results to display per page. $config['mod']['search_page'] = 200; - - // How many entries to show per page in the moderator noticeboard + // Number of entries to show per page in the moderator noticeboard. $config['mod']['noticeboard_page'] = 50; - // Number of entries to summarize and display on the dashboard + // Number of entries to summarize and display on the dashboard. $config['mod']['noticeboard_dashboard'] = 5; - - // Check public ban message by default + + // Check public ban message by default. $config['mod']['check_ban_message'] = false; - // Default public ban message. - // In public ban messages, %length% is replaced with "for x days" or "permanently" (with %LENGTH% being the uppercase equivalent). + // Default public ban message. In public ban messages, %length% is replaced with "for x days" or + // "permanently" (with %LENGTH% being the uppercase equivalent). $config['mod']['default_ban_message'] = _('USER WAS BANNED FOR THIS POST'); - // $config['mod']['default_ban_message'] = 'USER WAS BANNED %LENGTH% FOR THIS POST'; // Include length in ban message - // What to append to the post for public bans ("%s" is the message) + // $config['mod']['default_ban_message'] = 'USER WAS BANNED %LENGTH% FOR THIS POST'; + // HTML to append to post bodies for public bans messages (where "%s" is the message). $config['mod']['ban_message'] = '<span class="public_ban">(%s)</span>'; - - // When moving a thread to another board and choosing to keep a "shadow thread", an automated post (with a capcode) will - // be made, linking to the new location for the thread. "%s" will be replaced with a standard cross-board post citation (>>>/board/xxx) + + // When moving a thread to another board and choosing to keep a "shadow thread", an automated post (with + // a capcode) will be made, linking to the new location for the thread. "%s" will be replaced with a + // standard cross-board post citation (>>>/board/xxx) $config['mod']['shadow_mesage'] = 'Moved to %s.'; // Capcode to use when posting the above message. $config['mod']['shadow_capcode'] = 'Mod'; - // Name to use when posting the above message. If false, the default board name will be used. If something else, that will be used. + // Name to use when posting the above message. If false, $config['anonymous'] will be used. $config['mod']['shadow_name'] = false; - - // Wait indefinitely when rebuilding everything + + // PHP time limit for ?/rebuild. A value of 0 should cause PHP to wait indefinitely. $config['mod']['rebuild_timelimit'] = 0; - - // PM snippet (for ?/inbox) length in characters + + // PM snippet (for ?/inbox) length in characters. $config['mod']['snippet_length'] = 75; - - // Edit raw HTML in posts by default + + // Edit raw HTML in posts by default. $config['mod']['raw_html_default'] = false; - - // Automatically dismiss all reports regarding a thread when it is locked + + // Automatically dismiss all reports regarding a thread when it is locked. $config['mod']['dismiss_reports_on_lock'] = true; - - // Replace ?/config with a simple text editor for editing inc/instance-config.php + + // Replace ?/config with a simple text editor for editing inc/instance-config.php. $config['mod']['config_editor_php'] = false; - + +/* + * ==================== + * Mod permissions + * ==================== + */ + // Probably best not to change these: if (!defined('JANITOR')) { define('JANITOR', 0, true); @@ -949,31 +1030,24 @@ define('ADMIN', 2, true); define('DISABLED', 3, true); } - -/* - * ==================== - * Mod permissions - * ==================== - */ - - // Capcode permissions + + // Capcode permissions. $config['mod']['capcode'] = array( // JANITOR => array('Janitor'), - MOD => array('Mod'), + MOD => array('Mod'), ADMIN => true ); - + // Example: Allow mods to post with "## Moderator" as well // $config['mod']['capcode'][MOD][] = 'Moderator'; - // Example: Allow janitors to post with any capcode // $config['mod']['capcode'][JANITOR] = true; - - // Set any of the below to "DISABLED" to make them unavailable for everyone. - + + // Set any of the below to "DISABLED" to make them unavailable for everyone. + // Don't worry about per-board moderators. Let all mods moderate any board. $config['mod']['skip_per_board'] = false; - + /* Post Controls */ // View IP addresses $config['mod']['show_ip'] = MOD; @@ -1013,7 +1087,7 @@ $config['mod']['flood'] = ADMIN; // Raw HTML posting $config['mod']['rawhtml'] = ADMIN; - + /* Administration */ // View the report queue $config['mod']['reports'] = JANITOR; @@ -1025,8 +1099,8 @@ $config['mod']['view_banlist'] = MOD; // View the username of the mod who made a ban $config['mod']['view_banstaff'] = MOD; - // If the moderator doesn't fit the $config['mod']['view_banstaff''] (previous) permission, - // show him just a "?" instead. Otherwise, it will be "Mod" or "Admin" + // If the moderator doesn't fit the $config['mod']['view_banstaff''] (previous) permission, show him just + // a "?" instead. Otherwise, it will be "Mod" or "Admin". $config['mod']['view_banquestionmark'] = false; // Show expired bans in the ban list (they are kept in cache until the culprit returns) $config['mod']['view_banexpired'] = true; @@ -1060,8 +1134,8 @@ $config['mod']['modlog'] = ADMIN; // View IP addresses of other mods in ?/log $config['mod']['show_ip_modlog'] = ADMIN; - // View relevant moderation log entries on IP address pages (ie. ban history, etc.) - // Warning: Can be pretty resource exhaustive if your mod logs are huge. + // View relevant moderation log entries on IP address pages (ie. ban history, etc.) Warning: Can be + // pretty resource intensive if your mod logs are huge. $config['mod']['modlog_ip'] = MOD; // Create a PM (viewing mod usernames) $config['mod']['create_pm'] = JANITOR; @@ -1071,7 +1145,8 @@ $config['mod']['rebuild'] = ADMIN; // Search through posts, IP address notes and bans $config['mod']['search'] = JANITOR; - // Allow searching posts (can be used with board configuration file to disallow searching through a certain board) + // Allow searching posts (can be used with board configuration file to disallow searching through a + // certain board) $config['mod']['search_posts'] = JANITOR; // Read the moderator noticeboard $config['mod']['noticeboard'] = JANITOR; @@ -1089,13 +1164,13 @@ $config['mod']['news_custom'] = ADMIN; // Delete news entries $config['mod']['news_delete'] = ADMIN; - + // Edit the current configuration (via web interface) $config['mod']['edit_config'] = ADMIN; - + // Execute un-filtered SQL queries on the database (?/debug/sql) $config['mod']['debug_sql'] = DISABLED; - + /* * ==================== * Public post search @@ -1129,7 +1204,7 @@ // event_handler('post', function($post) { // // do something // }); - + // event_handler('post', function($post) { // // do something else // @@ -1142,51 +1217,51 @@ * Other/uncategorized * ==================== */ - - // Meta keywords. It's probably best to include these in per-board configurations. - //$config['meta_keywords'] = 'chan,anonymous discussion,imageboard,tinyboard'; - - // Link imageboard to your Google Analytics account to track users and provide marketing insights. + + // Meta keywords. It's probably best to include these in per-board configurations. + // $config['meta_keywords'] = 'chan,anonymous discussion,imageboard,tinyboard'; + + // Link imageboard to your Google Analytics account to track users and provide traffic insights. // $config['google_analytics'] = 'UA-xxxxxxx-yy'; // Keep the Google Analytics cookies to one domain -- ga._setDomainName() // $config['google_analytics_domain'] = 'www.example.org'; - - // If you use Varnish, Squid, or any similar caching reverse-proxy in front of Tinyboard, - // you can configure Tinyboard to PURGE files when they're written to - //$config['purge'] = array( - // array('127.0.0.1', 80) - // array('127.0.0.1', 80, 'example.org') - //); - // Connection timeout, in seconds + + // If you use Varnish, Squid, or any similar caching reverse-proxy in front of Tinyboard, you can + // configure Tinyboard to PURGE files when they're written to. + // $config['purge'] = array( + // array('127.0.0.1', 80) + // array('127.0.0.1', 80, 'example.org') + // ); + + // Connection timeout for $config['purge'], in seconds. $config['purge_timeout'] = 3; - - // Additional mod.php?/ pages (for developers). Look in inc/mod/pages.php for help. - //$config['mod']['custom_pages']['/something/(\d+)'] = function($id) { - // global $config; - // if (!hasPermission($config['mod']['something'])) - // error($config['error']['noaccess']); - // // ... - //}; - - // Add links to dashboard (will all be in "Other" category) - $config['mod']['dashboard_links'] = array(); + + // Additional mod.php?/ pages. Look in inc/mod/pages.php for help. + // $config['mod']['custom_pages']['/something/(\d+)'] = function($id) { + // global $config; + // if (!hasPermission($config['mod']['something'])) + // error($config['error']['noaccess']); + // // ... + // }; + + // Example: Add links to dashboard (will all be in a new "Other" category). // $config['mod']['dashboard_links']['Something'] = '?/something'; - - // Remote servers - // http://tinyboard.org/wiki/index.php?title=Multiple_Servers - //$config['remote']['static'] = array( - // 'host' => 'static.example.org', - // 'auth' => array( - // 'method' => 'plain', - // 'username' => 'username', - // 'password' => 'password!123' - // ), - // 'type' => 'scp' - //); - - // Regex for board URIs + + // Remote servers. I'm not even sure if this code works anymore. It might. Haven't tried it in a while. + // $config['remote']['static'] = array( + // 'host' => 'static.example.org', + // 'auth' => array( + // 'method' => 'plain', + // 'username' => 'username', + // 'password' => 'password!123' + // ), + // 'type' => 'scp' + // ); + + // Regex for board URIs. Don't add "`" character or any Unicode that MySQL can't handle. 58 characters + // is the absolute maximum, because MySQL cannot handle table names greater than 64 characters. $config['board_regex'] = '[0-9a-zA-Z$_\x{0080}-\x{FFFF}]{1,58}'; - - // Complex regular expression to catch URLs + + // Complex regular expression to catch URLs. $config['url_regex'] = '/' . '(https?|ftp):\/\/' . '(([\w\-]+\.)+[a-zA-Z]{2,6}|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' . '(:\d+)?' . '(\/([\w\-~.#\/?=&;:+%!*\[\]@$\'()+,|\^]+)?)?' . '/'; diff --git a/inc/database.php b/inc/database.php index 95dfc775..24959d0c 100644 --- a/inc/database.php +++ b/inc/database.php @@ -121,14 +121,14 @@ function query($query) { return $pdo->query($query); } -function db_error($PDOStatement=null) { - global $pdo; +function db_error($PDOStatement = null) { + global $pdo, $db_error; if (isset($PDOStatement)) { - $err = $PDOStatement->errorInfo(); - return $err[2]; + $db_error = $PDOStatement->errorInfo(); + return $db_error[2]; } - $err = $pdo->errorInfo(); - return $err[2]; + $db_error = $pdo->errorInfo(); + return $db_error[2]; } diff --git a/inc/display.php b/inc/display.php index fd21e83c..f00cf937 100644 --- a/inc/display.php +++ b/inc/display.php @@ -57,8 +57,8 @@ function createBoardlist($mod=false) { ); } -function error($message, $priority = true) { - global $board, $mod, $config; +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. @@ -70,17 +70,21 @@ function error($message, $priority = true) { die('Error: ' . $message . "\n"); } + if ($config['debug'] && isset($db_error)) { + $debug_stuff = array_combine(array('SQLSTATE', 'Error code', 'Error message'), $db_error); + } + die(Element('page.html', array( - 'config'=>$config, - 'title'=>_('Error'), - 'subtitle'=>_('An error has occured.'), - 'body'=>'<center>' . - '<h2>' . _($message) . '</h2>' . - (isset($board) ? - "<p><a href=\"" . $config['root'] . - ($mod ? $config['file_mod'] . '?/' : '') . - $board['dir'] . $config['file_index'] . "\">"._("Go back")."</a>.</p>" : '') . - '</center>' + '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) + )) ))); } @@ -319,31 +323,31 @@ class Post { // Delete if (hasPermission($config['mod']['delete'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_delete'], 'Delete', 'Are you sure you want to delete this?', $board['uri'] . '/delete/' . $this->id); + $built .= ' ' . secure_link_confirm($config['mod']['link_delete'], 'Delete', 'Are you sure you want to delete this?', $board['dir'] . 'delete/' . $this->id); // Delete all posts by IP if (hasPermission($config['mod']['deletebyip'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_deletebyip'], 'Delete all posts by IP', 'Are you sure you want to delete all posts by this IP address?', $board['uri'] . '/deletebyip/' . $this->id); + $built .= ' ' . secure_link_confirm($config['mod']['link_deletebyip'], 'Delete all posts by IP', 'Are you sure you want to delete all posts by this IP address?', $board['dir'] . 'deletebyip/' . $this->id); // Delete all posts by IP (global) if (hasPermission($config['mod']['deletebyip_global'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_deletebyip_global'], 'Delete all posts by IP across all boards', 'Are you sure you want to delete all posts by this IP address, across all boards?', $board['uri'] . '/deletebyip/' . $this->id . '/global'); + $built .= ' ' . secure_link_confirm($config['mod']['link_deletebyip_global'], 'Delete all posts by IP across all boards', 'Are you sure you want to delete all posts by this IP address, across all boards?', $board['dir'] . 'deletebyip/' . $this->id . '/global'); // Ban if (hasPermission($config['mod']['ban'], $board['uri'], $this->mod)) - $built .= ' <a title="'._('Ban').'" href="?/' . $board['uri'] . '/ban/' . $this->id . '">' . $config['mod']['link_ban'] . '</a>'; + $built .= ' <a title="'._('Ban').'" href="?/' . $board['dir'] . 'ban/' . $this->id . '">' . $config['mod']['link_ban'] . '</a>'; // Ban & Delete if (hasPermission($config['mod']['bandelete'], $board['uri'], $this->mod)) - $built .= ' <a title="'._('Ban & Delete').'" href="?/' . $board['uri'] . '/ban&delete/' . $this->id . '">' . $config['mod']['link_bandelete'] . '</a>'; + $built .= ' <a title="'._('Ban & Delete').'" href="?/' . $board['dir'] . 'ban&delete/' . $this->id . '">' . $config['mod']['link_bandelete'] . '</a>'; // Delete file (keep post) if (!empty($this->file) && hasPermission($config['mod']['deletefile'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_deletefile'], _('Delete file'), _('Are you sure you want to delete this file?'), $board['uri'] . '/deletefile/' . $this->id); + $built .= ' ' . secure_link_confirm($config['mod']['link_deletefile'], _('Delete file'), _('Are you sure you want to delete this file?'), $board['dir'] . 'deletefile/' . $this->id); // Edit post if (hasPermission($config['mod']['editpost'], $board['uri'], $this->mod)) - $built .= ' <a title="'._('Edit post').'" href="?/' . $board['uri'] . '/edit' . ($config['mod']['raw_html_default'] ? '_raw' : '') . '/' . $this->id . '">' . $config['mod']['link_editpost'] . '</a>'; + $built .= ' <a title="'._('Edit post').'" href="?/' . $board['dir'] . 'edit' . ($config['mod']['raw_html_default'] ? '_raw' : '') . '/' . $this->id . '">' . $config['mod']['link_editpost'] . '</a>'; if (!empty($built)) $built = '<span class="controls">' . $built . '</span>'; @@ -420,54 +424,54 @@ class Thread { // Mod controls (on posts) // Delete if (hasPermission($config['mod']['delete'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_delete'], _('Delete'), _('Are you sure you want to delete this?'), $board['uri'] . '/delete/' . $this->id); + $built .= ' ' . secure_link_confirm($config['mod']['link_delete'], _('Delete'), _('Are you sure you want to delete this?'), $board['dir'] . 'delete/' . $this->id); // Delete all posts by IP if (hasPermission($config['mod']['deletebyip'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_deletebyip'], _('Delete all posts by IP'), _('Are you sure you want to delete all posts by this IP address?'), $board['uri'] . '/deletebyip/' . $this->id); + $built .= ' ' . secure_link_confirm($config['mod']['link_deletebyip'], _('Delete all posts by IP'), _('Are you sure you want to delete all posts by this IP address?'), $board['dir'] . 'deletebyip/' . $this->id); // Delete all posts by IP (global) if (hasPermission($config['mod']['deletebyip_global'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_deletebyip_global'], _('Delete all posts by IP across all boards'), _('Are you sure you want to delete all posts by this IP address, across all boards?'), $board['uri'] . '/deletebyip/' . $this->id . '/global'); + $built .= ' ' . secure_link_confirm($config['mod']['link_deletebyip_global'], _('Delete all posts by IP across all boards'), _('Are you sure you want to delete all posts by this IP address, across all boards?'), $board['dir'] . 'deletebyip/' . $this->id . '/global'); // Ban if (hasPermission($config['mod']['ban'], $board['uri'], $this->mod)) - $built .= ' <a title="'._('Ban').'" href="?/' . $board['uri'] . '/ban/' . $this->id . '">' . $config['mod']['link_ban'] . '</a>'; + $built .= ' <a title="'._('Ban').'" href="?/' . $board['dir'] . 'ban/' . $this->id . '">' . $config['mod']['link_ban'] . '</a>'; // Ban & Delete if (hasPermission($config['mod']['bandelete'], $board['uri'], $this->mod)) - $built .= ' <a title="'._('Ban & Delete').'" href="?/' . $board['uri'] . '/ban&delete/' . $this->id . '">' . $config['mod']['link_bandelete'] . '</a>'; + $built .= ' <a title="'._('Ban & Delete').'" href="?/' . $board['dir'] . 'ban&delete/' . $this->id . '">' . $config['mod']['link_bandelete'] . '</a>'; // Delete file (keep post) if (!empty($this->file) && $this->file != 'deleted' && hasPermission($config['mod']['deletefile'], $board['uri'], $this->mod)) - $built .= ' ' . secure_link_confirm($config['mod']['link_deletefile'], _('Delete file'), _('Are you sure you want to delete this file?'), $board['uri'] . '/deletefile/' . $this->id); + $built .= ' ' . secure_link_confirm($config['mod']['link_deletefile'], _('Delete file'), _('Are you sure you want to delete this file?'), $board['dir'] . 'deletefile/' . $this->id); // Sticky if (hasPermission($config['mod']['sticky'], $board['uri'], $this->mod)) if ($this->sticky) - $built .= ' <a title="'._('Make thread not sticky').'" href="?/' . secure_link($board['uri'] . '/unsticky/' . $this->id) . '">' . $config['mod']['link_desticky'] . '</a>'; + $built .= ' <a title="'._('Make thread not sticky').'" href="?/' . secure_link($board['dir'] . 'unsticky/' . $this->id) . '">' . $config['mod']['link_desticky'] . '</a>'; else - $built .= ' <a title="'._('Make thread sticky').'" href="?/' . secure_link($board['uri'] . '/sticky/' . $this->id) . '">' . $config['mod']['link_sticky'] . '</a>'; + $built .= ' <a title="'._('Make thread sticky').'" href="?/' . secure_link($board['dir'] . 'sticky/' . $this->id) . '">' . $config['mod']['link_sticky'] . '</a>'; if (hasPermission($config['mod']['bumplock'], $board['uri'], $this->mod)) if ($this->bumplocked) - $built .= ' <a title="'._('Allow thread to be bumped').'" href="?/' . secure_link($board['uri'] . '/bumpunlock/' . $this->id) . '">' . $config['mod']['link_bumpunlock'] . '</a>'; + $built .= ' <a title="'._('Allow thread to be bumped').'" href="?/' . secure_link($board['dir'] . 'bumpunlock/' . $this->id) . '">' . $config['mod']['link_bumpunlock'] . '</a>'; else - $built .= ' <a title="'._('Prevent thread from being bumped').'" href="?/' . secure_link($board['uri'] . '/bumplock/' . $this->id) . '">' . $config['mod']['link_bumplock'] . '</a>'; + $built .= ' <a title="'._('Prevent thread from being bumped').'" href="?/' . secure_link($board['dir'] . 'bumplock/' . $this->id) . '">' . $config['mod']['link_bumplock'] . '</a>'; // Lock if (hasPermission($config['mod']['lock'], $board['uri'], $this->mod)) if ($this->locked) - $built .= ' <a title="'._('Unlock thread').'" href="?/' . secure_link($board['uri'] . '/unlock/' . $this->id) . '">' . $config['mod']['link_unlock'] . '</a>'; + $built .= ' <a title="'._('Unlock thread').'" href="?/' . secure_link($board['dir'] . 'unlock/' . $this->id) . '">' . $config['mod']['link_unlock'] . '</a>'; else - $built .= ' <a title="'._('Lock thread').'" href="?/' . secure_link($board['uri'] . '/lock/' . $this->id) . '">' . $config['mod']['link_lock'] . '</a>'; + $built .= ' <a title="'._('Lock thread').'" href="?/' . secure_link($board['dir'] . 'lock/' . $this->id) . '">' . $config['mod']['link_lock'] . '</a>'; if (hasPermission($config['mod']['move'], $board['uri'], $this->mod)) - $built .= ' <a title="'._('Move thread to another board').'" href="?/' . $board['uri'] . '/move/' . $this->id . '">' . $config['mod']['link_move'] . '</a>'; + $built .= ' <a title="'._('Move thread to another board').'" href="?/' . $board['dir'] . 'move/' . $this->id . '">' . $config['mod']['link_move'] . '</a>'; // Edit post if (hasPermission($config['mod']['editpost'], $board['uri'], $this->mod)) - $built .= ' <a title="'._('Edit post').'" href="?/' . $board['uri'] . '/edit' . ($config['mod']['raw_html_default'] ? '_raw' : '') . '/' . $this->id . '">' . $config['mod']['link_editpost'] . '</a>'; + $built .= ' <a title="'._('Edit post').'" href="?/' . $board['dir'] . 'edit' . ($config['mod']['raw_html_default'] ? '_raw' : '') . '/' . $this->id . '">' . $config['mod']['link_editpost'] . '</a>'; if (!empty($built)) $built = '<span class="controls op">' . $built . '</span>'; diff --git a/inc/filters.php b/inc/filters.php index 9f03154d..cd512821 100644 --- a/inc/filters.php +++ b/inc/filters.php @@ -132,10 +132,10 @@ class Filter { function do_filters(array $post) { global $config; - if (!isset($config['flood_filters'])) + if (!isset($config['filters'])) return; - foreach ($config['flood_filters'] as $arr) { + foreach ($config['filters'] as $arr) { $filter = new Filter($arr); if ($filter->check($post)) $filter->action(); diff --git a/inc/functions.php b/inc/functions.php index 1a8a52f2..7740c8f7 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -25,14 +25,14 @@ loadConfig(); function loadConfig() { global $board, $config, $__ip, $debug, $__version; - + $error = function_exists('error') ? 'error' : 'basic_error_function_because_the_other_isnt_loaded_yet'; - + reset_events(); - + if (!isset($_SERVER['REMOTE_ADDR'])) $_SERVER['REMOTE_ADDR'] = '0.0.0.0'; - + $arrays = array( 'db', 'cache', @@ -41,7 +41,7 @@ function loadConfig() { 'dir', 'mod', 'spam', - 'flood_filters', + 'filters', 'wordfilters', 'custom_capcode', 'custom_tripcode', @@ -55,43 +55,44 @@ function loadConfig() { 'stylesheets', 'additional_javascript', 'markup', - 'custom_pages' + 'custom_pages', + 'dashboard_links' ); - + $config = array(); foreach ($arrays as $key) { $config[$key] = array(); } - + require 'inc/config.php'; if (!file_exists('inc/instance-config.php')) $error('Tinyboard is not configured! Create inc/instance-config.php.'); - + require 'inc/instance-config.php'; - + if (isset($board['dir']) && file_exists($board['dir'] . '/config.php')) { require $board['dir'] . '/config.php'; } - + if (!isset($__version)) $__version = file_exists('.installed') ? trim(file_get_contents('.installed')) : false; $config['version'] = $__version; - + if ($config['debug']) { if (!isset($debug)) { $debug = array('sql' => array(), 'purge' => array(), 'cached' => array(), 'write' => array()); $debug['start'] = microtime(true); } } - + date_default_timezone_set($config['timezone']); - - if (!isset($config['blotter'])) - $config['blotter'] = false; - + + if (!isset($config['global_message'])) + $config['global_message'] = false; + if (!isset($config['post_url'])) $config['post_url'] = $config['root'] . $config['file_post']; - + if (!isset($config['referer_match'])) if (isset($_SERVER['HTTP_HOST'])) { $config['referer_match'] = '/^' . @@ -117,10 +118,10 @@ function loadConfig() { } if (!isset($config['cookies']['path'])) $config['cookies']['path'] = &$config['root']; - + if (!isset($config['dir']['static'])) $config['dir']['static'] = $config['root'] . 'static/'; - + if (!isset($config['image_sticky'])) $config['image_sticky'] = $config['dir']['static'] . 'sticky.gif'; if (!isset($config['image_locked'])) @@ -129,50 +130,50 @@ function loadConfig() { $config['image_bumplocked'] = $config['dir']['static'] . 'sage.gif'; if (!isset($config['image_deleted'])) $config['image_deleted'] = $config['dir']['static'] . 'deleted.png'; - if (!isset($config['image_zip'])) - $config['image_zip'] = $config['dir']['static'] . 'zip.png'; - + if (!isset($config['uri_thumb'])) $config['uri_thumb'] = $config['root'] . $board['dir'] . $config['dir']['thumb']; elseif (isset($board['dir'])) $config['uri_thumb'] = sprintf($config['uri_thumb'], $board['dir']); - + if (!isset($config['uri_img'])) $config['uri_img'] = $config['root'] . $board['dir'] . $config['dir']['img']; elseif (isset($board['dir'])) $config['uri_img'] = sprintf($config['uri_img'], $board['dir']); - + if (!isset($config['uri_stylesheets'])) $config['uri_stylesheets'] = $config['root'] . 'stylesheets/'; - + if (!isset($config['url_stylesheet'])) $config['url_stylesheet'] = $config['uri_stylesheets'] . 'style.css'; if (!isset($config['url_javascript'])) $config['url_javascript'] = $config['root'] . $config['file_script']; if (!isset($config['additional_javascript_url'])) $config['additional_javascript_url'] = $config['root']; - + if ($config['root_file']) { chdir($config['root_file']); } if ($config['verbose_errors']) { + set_error_handler('verbose_error_handler'); error_reporting(E_ALL); - ini_set('display_errors', 1); + ini_set('display_errors', true); + ini_set('html_errors', false); } - + // Keep the original address to properly comply with other board configurations if (!isset($__ip)) $__ip = $_SERVER['REMOTE_ADDR']; - + // ::ffff:0.0.0.0 if (preg_match('/^\:\:(ffff\:)?(\d+\.\d+\.\d+\.\d+)$/', $__ip, $m)) $_SERVER['REMOTE_ADDR'] = $m[2]; - + if (_setlocale(LC_ALL, $config['locale']) === false) { $error('The specified locale (' . $config['locale'] . ') does not exist on your platform!'); } - + if (extension_loaded('gettext')) { bindtextdomain('tinyboard', './inc/locale'); bind_textdomain_codeset('tinyboard', 'UTF-8'); @@ -182,11 +183,11 @@ function loadConfig() { _bind_textdomain_codeset('tinyboard', 'UTF-8'); _textdomain('tinyboard'); } - - + + if ($config['syslog']) openlog('tinyboard', LOG_ODELAY, LOG_SYSLOG); // open a connection to sysem logger - + if ($config['recaptcha']) require_once 'inc/lib/recaptcha/recaptchalib.php'; if ($config['cache']['enabled']) @@ -196,12 +197,12 @@ function loadConfig() { function basic_error_function_because_the_other_isnt_loaded_yet($message, $priority = true) { global $config; - + 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); } - + // Yes, this is horrible. die('<!DOCTYPE html><html><head><title>Error</title>' . '<style type="text/css">' . @@ -234,9 +235,20 @@ function _syslog($priority, $message) { } } +function verbose_error_handler($errno, $errstr, $errfile, $errline) { + if (error_reporting() == 0) + return false; // Looks like this warning was suppressed by the @ operator. + error(utf8tohtml($errstr), true, array( + 'file' => $errfile . ':' . $errline, + 'errno' => $errno, + 'error' => $errstr, + 'backtrace' => array_slice(debug_backtrace(), 1) + )); +} + function create_antibot($board, $thread = null) { require_once dirname(__FILE__) . '/anti-bot.php'; - + return _create_antibot($board, $thread); } @@ -270,7 +282,7 @@ function rebuildTheme($theme, $action, $board = false) { if (file_exists($config['dir']['themes'] . '/' . $_theme . '/theme.php')) { require_once $config['dir']['themes'] . '/' . $_theme . '/theme.php'; - + $theme['build_function']($action, themeSettings($_theme), $board); } } @@ -295,7 +307,7 @@ function sprintf3($str, $vars, $delim = '%') { $replaces[$delim . $k . $delim] = $v; } return str_replace(array_keys($replaces), - array_values($replaces), $str); + array_values($replaces), $str); } function mb_substr_replace($string, $replacement, $start, $length) { @@ -304,21 +316,21 @@ function mb_substr_replace($string, $replacement, $start, $length) { function setupBoard($array) { global $board, $config; - + $board = array( 'uri' => $array['uri'], 'title' => $array['title'], 'subtitle' => $array['subtitle'] ); - + // older versions $board['name'] = &$board['title']; - + $board['dir'] = sprintf($config['board_path'], $board['uri']); $board['url'] = sprintf($config['board_abbreviation'], $board['uri']); - + loadConfig(); - + if (!file_exists($board['dir'])) @mkdir($board['dir'], 0777) or error("Couldn't create " . $board['dir'] . ". Check permissions.", true); if (!file_exists($board['dir'] . $config['dir']['img'])) @@ -333,6 +345,11 @@ function setupBoard($array) { } function openBoard($uri) { + global $config, $build_pages; + + if ($config['try_smarter']) + $build_pages = array(); + $board = getBoardInfo($uri); if ($board) { setupBoard($board); @@ -347,11 +364,11 @@ function getBoardInfo($uri) { if ($config['cache']['enabled'] && ($board = cache::get('board_' . $uri))) { return $board; } - + $query = prepare("SELECT * FROM ``boards`` WHERE `uri` = :uri LIMIT 1"); $query->bindValue(':uri', $uri); $query->execute() or error(db_error($query)); - + if ($board = $query->fetch(PDO::FETCH_ASSOC)) { if ($config['cache']['enabled']) cache::set('board_' . $uri, $board); @@ -370,22 +387,22 @@ function boardTitle($uri) { function purge($uri) { global $config, $debug; - + // Fix for Unicode $uri = urlencode($uri); $uri = str_replace("%2F", "/", $uri); $uri = str_replace("%3A", ":", $uri); - + if (preg_match($config['referer_match'], $config['root']) && isset($_SERVER['REQUEST_URI'])) { $uri = (str_replace('\\', '/', dirname($_SERVER['REQUEST_URI'])) == '/' ? '/' : str_replace('\\', '/', dirname($_SERVER['REQUEST_URI'])) . '/') . $uri; } else { $uri = $config['root'] . $uri; } - + if ($config['debug']) { $debug['purge'][] = $uri; } - + foreach ($config['purge'] as &$purge) { $host = &$purge[0]; $port = &$purge[1]; @@ -403,11 +420,11 @@ function purge($uri) { function file_write($path, $data, $simple = false, $skip_purge = false) { global $config, $debug; - + if (preg_match('/^remote:\/\/(.+)\:(.+)$/', $path, $m)) { if (isset($config['remote'][$m[1]])) { require_once 'inc/remote.php'; - + $remote = new Remote($config['remote'][$m[1]]); $remote->write($data, $m[2]); return; @@ -415,31 +432,31 @@ function file_write($path, $data, $simple = false, $skip_purge = false) { error('Invalid remote server: ' . $m[1]); } } - + if (!$fp = fopen($path, $simple ? 'w' : 'c')) error('Unable to open file for writing: ' . $path); - + // File locking if (!$simple && !flock($fp, LOCK_EX)) { error('Unable to lock file: ' . $path); } - + // Truncate file if (!$simple && !ftruncate($fp, 0)) error('Unable to truncate file: ' . $path); - + // Write data if (($bytes = fwrite($fp, $data)) === false) error('Unable to write to file: ' . $path); - + // Unlock if (!$simple) flock($fp, LOCK_UN); - + // Close if (!fclose($fp)) error('Unable to close file: ' . $path); - + if (!$skip_purge && isset($config['purge'])) { // Purge cache if (basename($path) == $config['file_index']) { @@ -454,23 +471,23 @@ function file_write($path, $data, $simple = false, $skip_purge = false) { } purge($path); } - + if ($config['debug']) { $debug['write'][] = $path . ': ' . $bytes . ' bytes'; } - + event('write', $path); } function file_unlink($path) { global $config, $debug; - + if ($config['debug']) { if (!isset($debug['unlink'])) $debug['unlink'] = array(); $debug['unlink'][] = $path; } - + $ret = @unlink($path); if (isset($config['purge']) && $path[0] != '/' && isset($_SERVER['HTTP_HOST'])) { // Purge cache @@ -486,56 +503,56 @@ function file_unlink($path) { } purge($path); } - + event('unlink', $path); - + return $ret; } function hasPermission($action = null, $board = null, $_mod = null) { global $config; - + if (isset($_mod)) $mod = &$_mod; else global $mod; - + if (!is_array($mod)) return false; - + if (isset($action) && $mod['type'] < $action) return false; - + if (!isset($board) || $config['mod']['skip_per_board']) return true; - + if (!isset($mod['boards'])) return false; - + if (!in_array('*', $mod['boards']) && !in_array($board, $mod['boards'])) return false; - + return true; } function listBoards() { global $config; - + if ($config['cache']['enabled'] && ($boards = cache::get('all_boards'))) return $boards; - + $query = query("SELECT * FROM ``boards`` ORDER BY `uri`") or error(db_error()); $boards = $query->fetchAll(); - + if ($config['cache']['enabled']) cache::set('all_boards', $boards); - + return $boards; } function checkFlood($post) { global $board, $config; - + $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`ip` = :ip AND `time` >= :floodtime) OR (`ip` = :ip AND `body` != '' AND `body` = :body AND `time` >= :floodsameiptime) OR (`body` != '' AND `body` = :body AND `time` >= :floodsametime) LIMIT 1", $board['uri'])); $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); $query->bindValue(':body', $post['body']); @@ -543,12 +560,12 @@ function checkFlood($post) { $query->bindValue(':floodsameiptime', time()-$config['flood_time_ip'], PDO::PARAM_INT); $query->bindValue(':floodsametime', time()-$config['flood_time_same'], PDO::PARAM_INT); $query->execute() or error(db_error($query)); - + $flood = (bool) $query->fetch(PDO::FETCH_ASSOC); - + if (event('check-flood', $post)) return true; - + return $flood; } @@ -588,15 +605,15 @@ function ago($timestamp) { function displayBan($ban) { global $config; - + if (!$ban['seen']) { $query = prepare("UPDATE ``bans`` SET `seen` = 1 WHERE `id` = :id"); $query->bindValue(':id', $ban['id'], PDO::PARAM_INT); $query->execute() or error(db_error($query)); } - + $ban['ip'] = $_SERVER['REMOTE_ADDR']; - + // Show banned page and exit die( Element('page.html', array( @@ -612,15 +629,15 @@ function displayBan($ban) { function checkBan($board = 0) { global $config; - + if (!isset($_SERVER['REMOTE_ADDR'])) { // Server misconfiguration return; } - + if (event('check-ban', $board)) return true; - + $query = prepare("SELECT `set`, `expires`, `reason`, `board`, `seen`, ``bans``.`id` FROM ``bans`` WHERE (`board` IS NULL OR `board` = :board) AND `ip` = :ip ORDER BY `expires` IS NULL DESC, `expires` DESC, `expires` DESC LIMIT 1"); $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); $query->bindValue(':board', $board); @@ -631,7 +648,7 @@ function checkBan($board = 0) { $query->bindValue(':board', $board); $query->execute() or error(db_error($query)); } - + if ($query->rowCount() < 1 && $config['ban_cidr'] && !isIPv6()) { // my most insane SQL query yet $query = prepare("SELECT `set`, `expires`, `reason`, `board`, `seen`, ``bans``.`id` FROM ``bans`` WHERE (`board` IS NULL OR `board` = :board) @@ -647,24 +664,24 @@ function checkBan($board = 0) { $query->bindValue(':board', $board); $query->execute() or error(db_error($query)); } - + if ($ban = $query->fetch(PDO::FETCH_ASSOC)) { if ($ban['expires'] && $ban['expires'] < time()) { // Ban expired $query = prepare("DELETE FROM ``bans`` WHERE `id` = :id"); $query->bindValue(':id', $ban['id'], PDO::PARAM_INT); $query->execute() or error(db_error($query)); - + if ($config['require_ban_view'] && !$ban['seen']) { displayBan($ban); } - + return; } - + displayBan($ban); } - + // I'm not sure where else to put this. It doesn't really matter where; it just needs to be called every now and then to keep the ban list tidy. purge_bans(); } @@ -678,47 +695,47 @@ function purge_bans() { function threadLocked($id) { global $board; - + if (event('check-locked', $id)) return true; - + $query = prepare(sprintf("SELECT `locked` FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL LIMIT 1", $board['uri'])); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error()); - + if (($locked = $query->fetchColumn()) === false) { // Non-existant, so it can't be locked... return false; } - + return (bool)$locked; } function threadSageLocked($id) { global $board; - + if (event('check-sage-locked', $id)) return true; - + $query = prepare(sprintf("SELECT `sage` FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL LIMIT 1", $board['uri'])); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error()); - + if (($sagelocked = $query->fetchColumn()) === false) { // Non-existant, so it can't be locked... return false; } - + return (bool)$sagelocked; } function threadExists($id) { global $board; - + $query = prepare(sprintf("SELECT 1 FROM ``posts_%s`` WHERE `id` = :id AND `thread` IS NULL LIMIT 1", $board['uri'])); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error()); - + if ($query->rowCount()) { return true; } @@ -729,70 +746,70 @@ function threadExists($id) { 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, :thumb, :thumbwidth, :thumbheight, :file, :width, :height, :filesize, :filename, :filehash, :password, :ip, :sticky, :locked, 0, :embed)", $board['uri'])); - + // Basic stuff if (!empty($post['subject'])) { $query->bindValue(':subject', $post['subject']); } else { $query->bindValue(':subject', NULL, PDO::PARAM_NULL); } - + if (!empty($post['email'])) { $query->bindValue(':email', $post['email']); } else { $query->bindValue(':email', NULL, PDO::PARAM_NULL); } - + if (!empty($post['trip'])) { $query->bindValue(':trip', $post['trip']); } else { $query->bindValue(':trip', NULL, PDO::PARAM_NULL); } - + $query->bindValue(':name', $post['name']); $query->bindValue(':body', $post['body']); $query->bindValue(':body_nomarkup', $post['body_nomarkup']); $query->bindValue(':time', isset($post['time']) ? $post['time'] : time(), PDO::PARAM_INT); $query->bindValue(':password', $post['password']); $query->bindValue(':ip', isset($post['ip']) ? $post['ip'] : $_SERVER['REMOTE_ADDR']); - + if ($post['op'] && $post['mod'] && isset($post['sticky']) && $post['sticky']) { $query->bindValue(':sticky', 1, PDO::PARAM_INT); } else { $query->bindValue(':sticky', 0, PDO::PARAM_INT); } - + if ($post['op'] && $post['mod'] && isset($post['locked']) && $post['locked']) { $query->bindValue(':locked', 1, PDO::PARAM_INT); } else { $query->bindValue(':locked', 0, PDO::PARAM_INT); } - + if ($post['mod'] && isset($post['capcode']) && $post['capcode']) { $query->bindValue(':capcode', $post['capcode'], PDO::PARAM_INT); } else { $query->bindValue(':capcode', NULL, PDO::PARAM_NULL); } - + if (!empty($post['embed'])) { $query->bindValue(':embed', $post['embed']); } else { $query->bindValue(':embed', NULL, PDO::PARAM_NULL); } - + if ($post['op']) { // No parent thread, image $query->bindValue(':thread', null, PDO::PARAM_NULL); } else { $query->bindValue(':thread', $post['thread'], PDO::PARAM_INT); } - + if ($post['has_file']) { $query->bindValue(':thumb', $post['thumb']); $query->bindValue(':thumbwidth', $post['thumbwidth'], PDO::PARAM_INT); $query->bindValue(':thumbheight', $post['thumbheight'], PDO::PARAM_INT); $query->bindValue(':file', $post['file']); - + if (isset($post['width'], $post['height'])) { $query->bindValue(':width', $post['width'], PDO::PARAM_INT); $query->bindValue(':height', $post['height'], PDO::PARAM_INT); @@ -800,7 +817,7 @@ function post(array $post) { $query->bindValue(':width', null, PDO::PARAM_NULL); $query->bindValue(':height', null, PDO::PARAM_NULL); } - + $query->bindValue(':filesize', $post['filesize'], PDO::PARAM_INT); $query->bindValue(':filename', $post['filename']); $query->bindValue(':filehash', $post['filehash']); @@ -815,20 +832,23 @@ function post(array $post) { $query->bindValue(':filename', null, PDO::PARAM_NULL); $query->bindValue(':filehash', null, PDO::PARAM_NULL); } - + if (!$query->execute()) { undoImage($post); error(db_error($query)); } - + return $pdo->lastInsertId(); } function bumpThread($id) { - global $board; - + global $config, $board, $build_pages; + if (event('bump', $id)) return true; + + if ($config['try_smarter']) + $build_pages[] = thread_find_page($id); $query = prepare(sprintf("UPDATE ``posts_%s`` SET `bump` = :time WHERE `id` = :id AND `thread` IS NULL", $board['uri'])); $query->bindValue(':time', time(), PDO::PARAM_INT); @@ -839,16 +859,16 @@ function bumpThread($id) { // Remove file from post function deleteFile($id, $remove_entirely_if_already=true) { global $board, $config; - + $query = prepare(sprintf("SELECT `thread`,`thumb`,`file` FROM ``posts_%s`` WHERE `id` = :id LIMIT 1", $board['uri'])); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); if (!$post = $query->fetch(PDO::FETCH_ASSOC)) error($config['error']['invalidpost']); - + if ($post['file'] == 'deleted' && !$post['thread']) return; // Can't delete OP's image completely. - + $query = prepare(sprintf("UPDATE ``posts_%s`` SET `thumb` = NULL, `thumbwidth` = NULL, `thumbheight` = NULL, `filewidth` = NULL, `fileheight` = NULL, `filesize` = NULL, `filename` = NULL, `filehash` = NULL, `file` = :file WHERE `id` = :id", $board['uri'])); if ($post['file'] == 'deleted' && $remove_entirely_if_already) { // Already deleted; remove file fully @@ -856,17 +876,17 @@ function deleteFile($id, $remove_entirely_if_already=true) { } else { // Delete thumbnail file_unlink($board['dir'] . $config['dir']['thumb'] . $post['thumb']); - + // Delete file file_unlink($board['dir'] . $config['dir']['img'] . $post['file']); - + // Set file to 'deleted' $query->bindValue(':file', 'deleted', PDO::PARAM_INT); } - + $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); - + if ($post['thread']) buildThread($post['thread']); else @@ -876,49 +896,49 @@ function deleteFile($id, $remove_entirely_if_already=true) { // rebuild post (markup) function rebuildPost($id) { global $board; - + $query = prepare(sprintf("SELECT `body_nomarkup`, `thread` 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']); - + $query = prepare(sprintf("UPDATE ``posts_%s`` SET `body` = :body WHERE `id` = :id", $board['uri'])); $query->bindValue(':body', $body); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); - + buildThread($post['thread'] ? $post['thread'] : $id); - + return true; } // Delete a post (reply or thread) function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) { global $board, $config; - + // Select post and replies (if thread) in one query $query = prepare(sprintf("SELECT `id`,`thread`,`thumb`,`file` FROM ``posts_%s`` WHERE `id` = :id OR `thread` = :id", $board['uri'])); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); - + if ($query->rowCount() < 1) { if ($error_if_doesnt_exist) error($config['error']['invalidpost']); else return false; } - + $ids = array(); - + // Delete posts and maybe replies while ($post = $query->fetch(PDO::FETCH_ASSOC)) { if (!$post['thread']) { // Delete thread HTML page file_unlink($board['dir'] . $config['dir']['res'] . sprintf($config['file_page'], $post['id'])); - + $antispam_query = prepare('DELETE FROM ``antispam`` WHERE `board` = :board AND `thread` = :thread'); $antispam_query->bindValue(':board', $board['uri']); $antispam_query->bindValue(':thread', $post['id']); @@ -935,15 +955,15 @@ function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) { // Delete file file_unlink($board['dir'] . $config['dir']['img'] . $post['file']); } - + $ids[] = (int)$post['id']; - + } - + $query = prepare(sprintf("DELETE FROM ``posts_%s`` WHERE `id` = :id OR `thread` = :id", $board['uri'])); $query->bindValue(':id', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); - + $query = prepare("SELECT `board`, `post` FROM ``cites`` WHERE `target_board` = :board AND (`target` = " . implode(' OR `target` = ', $ids) . ")"); $query->bindValue(':board', $board['uri']); $query->execute() or error(db_error($query)); @@ -955,10 +975,10 @@ function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) { } rebuildPost($cite['post']); } - + if (isset($tmp_board)) openBoard($tmp_board); - + $query = prepare("DELETE FROM ``cites`` WHERE (`target_board` = :board AND `target` = :id) OR (`board` = :board AND `post` = :id)"); $query->bindValue(':board', $board['uri']); $query->bindValue(':id', $id, PDO::PARAM_INT); @@ -967,38 +987,48 @@ function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) { if (isset($rebuild) && $rebuild_after) { buildThread($rebuild); } - + return true; } function clean() { global $board, $config; $offset = round($config['max_pages']*$config['threads_per_page']); - + // I too wish there was an easier way of doing this... $query = prepare(sprintf("SELECT `id` FROM ``posts_%s`` WHERE `thread` IS NULL ORDER BY `sticky` DESC, `bump` DESC LIMIT :offset, 9001", $board['uri'])); $query->bindValue(':offset', $offset, PDO::PARAM_INT); - + $query->execute() or error(db_error($query)); while ($post = $query->fetch(PDO::FETCH_ASSOC)) { deletePost($post['id']); } } +function thread_find_page($thread) { + global $config, $board; + + $query = query(sprintf("SELECT `id` FROM ``posts_%s`` WHERE `thread` IS NULL ORDER BY `sticky` DESC, `bump` DESC", $board['uri'])) or error(db_error($query)); + $threads = $query->fetchAll(PDO::FETCH_COLUMN); + if (($index = array_search($thread, $threads)) === false) + return false; + return floor(($config['threads_per_page'] + $index) / $config['threads_per_page']); +} + function index($page, $mod=false) { global $board, $config, $debug; $body = ''; $offset = round($page*$config['threads_per_page']-$config['threads_per_page']); - + $query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE `thread` IS NULL ORDER BY `sticky` DESC, `bump` DESC LIMIT :offset,:threads_per_page", $board['uri'])); $query->bindValue(':offset', $offset, PDO::PARAM_INT); $query->bindValue(':threads_per_page', $config['threads_per_page'], PDO::PARAM_INT); $query->execute() or error(db_error($query)); - + if ($page == 1 && $query->rowCount() < $config['threads_per_page']) $board['thread_count'] = $query->rowCount(); - + if ($query->rowCount() < 1 && $page > 1) return false; @@ -1008,7 +1038,7 @@ function index($page, $mod=false) { $th['thumbwidth'], $th['thumbheight'], $th['file'], $th['filewidth'], $th['fileheight'], $th['filesize'], $th['filename'], $th['ip'], $th['sticky'], $th['locked'], $th['sage'], $th['embed'], $mod ? '?/' : $config['root'], $mod ); - + if ($config['cache']['enabled'] && $cached = cache::get("thread_index_{$board['uri']}_{$th['id']}")) { $replies = $cached['replies']; $omitted = $cached['omitted']; @@ -1017,35 +1047,35 @@ function index($page, $mod=false) { $posts->bindValue(':id', $th['id']); $posts->bindValue(':limit', ($th['sticky'] ? $config['threads_preview_sticky'] : $config['threads_preview']), PDO::PARAM_INT); $posts->execute() or error(db_error($posts)); - + $replies = array_reverse($posts->fetchAll(PDO::FETCH_ASSOC)); - + if (count($replies) == ($th['sticky'] ? $config['threads_preview_sticky'] : $config['threads_preview'])) { $count = numPosts($th['id']); $omitted = array('post_count' => $count['replies'], 'image_count' => $count['images']); } else { $omitted = false; } - + if ($config['cache']['enabled']) cache::set("thread_index_{$board['uri']}_{$th['id']}", array( 'replies' => $replies, 'omitted' => $omitted, )); } - + $num_images = 0; foreach ($replies as $po) { if ($po['file']) $num_images++; - + $thread->add(new Post( $po['id'], $th['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 ($omitted) { $thread->omitted = $omitted['post_count'] - ($th['sticky'] ? $config['threads_preview_sticky'] : $config['threads_preview']); $thread->omitted_images = $omitted['image_count'] - $num_images; @@ -1054,7 +1084,7 @@ function index($page, $mod=false) { $threads[] = $thread; $body .= $thread->build(true); } - + return array( 'board' => $board, 'body' => $body, @@ -1067,10 +1097,10 @@ function index($page, $mod=false) { function getPageButtons($pages, $mod=false) { global $config, $board; - + $btn = array(); $root = ($mod ? '?/' : $config['root']) . $board['dir']; - + foreach ($pages as $num => $page) { if (isset($page['selected'])) { // Previous button @@ -1084,7 +1114,7 @@ function getPageButtons($pages, $mod=false) { : sprintf($config['file_page'], $num) ); - + $btn['prev'] = '<form action="' . ($mod ? '' : $root . $loc) . '" method="get">' . ($mod ? '<input type="hidden" name="status" value="301" />' . @@ -1092,13 +1122,13 @@ function getPageButtons($pages, $mod=false) { :'') . '<input type="submit" value="' . _('Previous') . '" /></form>'; } - + if ($num == count($pages) - 1) { // There is no next page. $btn['next'] = _('Next'); } else { $loc = ($mod ? '?/' . $board['uri'] . '/' : '') . sprintf($config['file_page'], $num + 2); - + $btn['next'] = '<form action="' . ($mod ? '' : $root . $loc) . '" method="get">' . ($mod ? '<input type="hidden" name="status" value="301" />' . @@ -1108,13 +1138,13 @@ function getPageButtons($pages, $mod=false) { } } } - + return $btn; } function getPages($mod=false) { global $board, $config; - + if (isset($board['thread_count'])) { $count = $board['thread_count']; } else { @@ -1123,9 +1153,9 @@ function getPages($mod=false) { $count = $query->fetchColumn(); } $count = floor(($config['threads_per_page'] + $count - 1) / $config['threads_per_page']); - + if ($count < 1) $count = 1; - + $pages = array(); for ($x=0;$x<$count && $x<$config['max_pages'];$x++) { $pages[] = array( @@ -1133,32 +1163,32 @@ function getPages($mod=false) { 'link' => $x==0 ? ($mod ? '?/' : $config['root']) . $board['dir'] . $config['file_index'] : ($mod ? '?/' : $config['root']) . $board['dir'] . sprintf($config['file_page'], $x+1) ); } - + return $pages; } function makerobot($body) { global $config; $body = strtolower($body); - + // Leave only letters $body = preg_replace('/[^a-z]/i', '', $body); // Remove repeating characters if ($config['robot_strip_repeating']) $body = preg_replace('/(.)\\1+/', '$1', $body); - + return sha1($body); } function checkRobot($body) { if (empty($body) || event('check-robot', $body)) return true; - + $body = makerobot($body); $query = prepare("SELECT 1 FROM ``robot`` WHERE `hash` = :hash LIMIT 1"); $query->bindValue(':hash', $body); $query->execute() or error(db_error($query)); - + if ($query->fetchColumn()) { return true; } @@ -1167,7 +1197,7 @@ function checkRobot($body) { $query = prepare("INSERT INTO ``robot`` VALUES (:hash)"); $query->bindValue(':hash', $body); $query->execute() or error(db_error($query)); - + return false; } @@ -1177,25 +1207,25 @@ function numPosts($id) { $query = prepare(sprintf("SELECT COUNT(*) FROM ``posts_%s`` WHERE `thread` = :thread UNION ALL SELECT COUNT(*) FROM ``posts_%s`` WHERE `file` IS NOT NULL AND `thread` = :thread", $board['uri'], $board['uri'])); $query->bindValue(':thread', $id, PDO::PARAM_INT); $query->execute() or error(db_error($query)); - + $num_posts = $query->fetchColumn(); $num_images = $query->fetchColumn(); - + return array('replies' => $num_posts, 'images' => $num_images); } function muteTime() { global $config; - + if ($time = event('mute-time')) return $time; - + // Find number of mutes in the past X hours $query = prepare("SELECT COUNT(*) FROM ``mutes`` WHERE `time` >= :time AND `ip` = :ip"); $query->bindValue(':time', time()-($config['robot_mute_hour']*3600), PDO::PARAM_INT); $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); $query->execute() or error(db_error($query)); - + if (!$result = $query->fetchColumn()) return 0; return pow($config['robot_mute_multiplier'], $result); @@ -1207,32 +1237,32 @@ function mute() { $query->bindValue(':time', time(), PDO::PARAM_INT); $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); $query->execute() or error(db_error($query)); - + return muteTime(); } function checkMute() { global $config, $debug; - + if ($config['cache']['enabled']) { // Cached mute? if (($mute = cache::get("mute_${_SERVER['REMOTE_ADDR']}")) && ($mutetime = cache::get("mutetime_${_SERVER['REMOTE_ADDR']}"))) { error(sprintf($config['error']['youaremuted'], $mute['time'] + $mutetime - time())); } } - + $mutetime = muteTime(); if ($mutetime > 0) { // Find last mute time $query = prepare("SELECT `time` FROM ``mutes`` WHERE `ip` = :ip ORDER BY `time` DESC LIMIT 1"); $query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); $query->execute() or error(db_error($query)); - + if (!$mute = $query->fetch(PDO::FETCH_ASSOC)) { // What!? He's muted but he's not muted... return; } - + if ($mute['time'] + $mutetime > time()) { if ($config['cache']['enabled']) { cache::set("mute_${_SERVER['REMOTE_ADDR']}", $mute, $mute['time'] + $mutetime - time()); @@ -1248,20 +1278,29 @@ function checkMute() { } function buildIndex() { - global $board, $config; - + global $board, $config, $build_pages; + $pages = getPages(); - $antibot = create_antibot($board['uri']); + if (!$config['try_smarter']) + $antibot = create_antibot($board['uri']); $api = new Api(); $catalog = array(); - $page = 1; - while ($page <= $config['max_pages'] && $content = index($page)) { + for ($page = 1; $page <= $config['max_pages']; $page++) { $filename = $board['dir'] . ($page == 1 ? $config['file_index'] : sprintf($config['file_page'], $page)); + if ($config['try_smarter'] && isset($build_pages) && count($build_pages) && !in_array($page, $build_pages) && is_file($filename)) + continue; + $content = index($page); + if (!$content) + break; + + if ($config['try_smarter']) { + $antibot = create_antibot($board['uri'], 0 - $page); + $content['current_page'] = $page; + } $antibot->reset(); - $content['pages'] = $pages; $content['pages'][$page-1]['selected'] = true; $content['btn'] = getPageButtons($content['pages']); @@ -1297,72 +1336,66 @@ function buildIndex() { function buildJavascript() { global $config; - + $stylesheets = array(); foreach ($config['stylesheets'] as $name => $uri) { $stylesheets[] = array( 'name' => addslashes($name), 'uri' => addslashes((!empty($uri) ? $config['uri_stylesheets'] : '') . $uri)); } - - // Check if we have translation for the javascripts; if yes, we add it to additional javascripts - list($pure_locale) = explode(".", $config['locale']); - if (file_exists ($jsloc = "inc/locale/".$pure_locale."/LC_MESSAGES/javascript.js")) { - array_unshift($config['additional_javascript'], $jsloc); - } $script = Element('main.js', array( 'config' => $config, 'stylesheets' => $stylesheets )); - + // Check if we have translation for the javascripts; if yes, we add it to additional javascripts list($pure_locale) = explode(".", $config['locale']); if (file_exists ($jsloc = "inc/locale/$pure_locale/LC_MESSAGES/javascript.js")) { $script = file_get_contents($jsloc) . "\n\n" . $script; } - + if ($config['additional_javascript_compile']) { foreach ($config['additional_javascript'] as $file) { $script .= file_get_contents($file); } } - + if ($config['minify_js']) { require_once 'inc/lib/minify/JSMin.php'; $script = JSMin::minify($script); } - + file_write($config['file_script'], $script); } function checkDNSBL() { global $config; - - + + if (isIPv6()) return; // No IPv6 support yet. - + if (!isset($_SERVER['REMOTE_ADDR'])) return; // Fix your web server configuration - + if (in_array($_SERVER['REMOTE_ADDR'], $config['dnsbl_exceptions'])) return; - + $ipaddr = ReverseIPOctets($_SERVER['REMOTE_ADDR']); - + foreach ($config['dnsbl'] as $blacklist) { if (!is_array($blacklist)) $blacklist = array($blacklist); - + if (($lookup = str_replace('%', $ipaddr, $blacklist[0])) == $blacklist[0]) $lookup = $ipaddr . '.' . $blacklist[0]; - + if (!$ip = DNS($lookup)) continue; // not in list - + $blacklist_name = isset($blacklist[2]) ? $blacklist[2] : $blacklist[0]; - + if (!isset($blacklist[1])) { // If you're listed at all, you're blocked. error(sprintf($config['error']['dnsbl'], $blacklist_name)); @@ -1391,7 +1424,7 @@ function ReverseIPOctets($ip) { function wordfilters(&$body) { global $config; - + foreach ($config['wordfilters'] as $filter) { if (isset($filter[2]) && $filter[2]) { $body = preg_replace($filter[0], $filter[1], $body); @@ -1403,29 +1436,29 @@ function wordfilters(&$body) { function quote($body, $quote=true) { global $config; - + $body = str_replace('<br/>', "\n", $body); - + $body = strip_tags($body); - + $body = preg_replace("/(^|\n)/", '$1>', $body); - + $body .= "\n"; - + if ($config['minify_html']) $body = str_replace("\n", '
', $body); - + return $body; } function markup_url($matches) { global $config, $markup_urls; - + $url = $matches[1]; $after = $matches[2]; - + $markup_urls[] = $url; - + return '<a target="_blank" rel="nofollow" href="'. $config['link_prefix'] . $url . '">' . $url . '</a>' . $after; } @@ -1433,36 +1466,36 @@ function unicodify($body) { $body = str_replace('...', '…', $body); $body = str_replace('<--', '←', $body); $body = str_replace('-->', '→', $body); - + // En and em- dashes are rendered exactly the same in // most monospace fonts (they look the same in code // editors). $body = str_replace('---', '—', $body); // em dash $body = str_replace('--', '–', $body); // en dash - + return $body; } function markup(&$body, $track_cites = false) { global $board, $config, $markup_urls; - + $body = str_replace("\r", '', $body); $body = utf8tohtml($body); - + if (preg_match_all('@<tinyboard ([\w\s]+)>(.+?)</tinyboard>@um', $body, $modifiers, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { $skip_chars = 0; $body_tmp = $body; $end_markup = false; - + foreach ($modifiers as $modifier) { // preg_match_all is not multibyte-safe foreach ($modifier as &$match) { $match[1] = mb_strlen(substr($body_tmp, 0, $match[1])); } - + $modifier['type'] = $modifier[1][0]; $modifier['content'] = $modifier[2][0]; - + if ($modifier['type'] == 'ban message') { // Public ban message $replacement = sprintf($config['mod']['ban_message'], html_entity_decode($modifier['content'])); @@ -1480,21 +1513,21 @@ function markup(&$body, $track_cites = false) { // Unknown $replacement = ''; } - + if (!$end_markup) { $body = mb_substr_replace($body, $replacement, $modifier[0][1] + $skip_chars, mb_strlen($modifier[0][0])); $skip_chars += mb_strlen($replacement) - mb_strlen($modifier[0][0]); } } - + if ($end_markup) { return array(); } } - + if (mysql_version() < 50503) $body = mb_encode_numericentity($body, array(0x010000, 0xffffff, 0, 0xffffff), 'UTF-8'); - + foreach ($config['markup'] as $markup) { if (is_string($markup[1])) { $body = preg_replace($markup[0], $markup[1], $body); @@ -1502,108 +1535,108 @@ function markup(&$body, $track_cites = false) { $body = preg_replace_callback($markup[0], $markup[1], $body); } } - + if ($config['markup_urls']) { $markup_urls = array(); - + $body = preg_replace_callback( '/((?:https?:\/\/|ftp:\/\/|irc:\/\/)[^\s<>()"]+?(?:\([^\s<>()"]*?\)[^\s<>()"]*?)*)((?:\s|<|>|"|\.||\]|!|\?|,|,|")*(?:[\s<>()"]|$))/', 'markup_url', $body, -1, $num_links); - + if ($num_links > $config['max_links']) error($config['error']['toomanylinks']); } - + if ($config['auto_unicode']) { $body = unicodify($body); - + if ($config['markup_urls']) { foreach ($markup_urls as &$url) { $body = str_replace(unicodify($url), $url, $body); } } } - + // replace tabs with 8 spaces $body = str_replace("\t", ' ', $body); - + $tracked_cites = array(); - + // Cites if (isset($board) && preg_match_all('/(^|\s)>>(\d+?)([\s,.)?]|$)/m', $body, $cites, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { if (count($cites[0]) > $config['max_cites']) { error($config['error']['toomanycites']); } - + $skip_chars = 0; $body_tmp = $body; - + foreach ($cites as $matches) { $cite = $matches[2][0]; $query = prepare(sprintf("SELECT `thread`,`id` FROM ``posts_%s`` WHERE `id` = :id LIMIT 1", $board['uri'])); $query->bindValue(':id', $cite); $query->execute() or error(db_error($query)); - + // preg_match_all is not multibyte-safe foreach ($matches as &$match) { $match[1] = mb_strlen(substr($body_tmp, 0, $match[1])); } - + if ($post = $query->fetch(PDO::FETCH_ASSOC)) { $replacement = '<a onclick="highlightReply(\''.$cite.'\');" href="' . $config['root'] . $board['dir'] . $config['dir']['res'] . ($post['thread']?$post['thread']:$post['id']) . '.html#' . $cite . '">' . '>>' . $cite . '</a>'; - + $body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[3][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0])); $skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[3][0]) - mb_strlen($matches[0][0]); - + if ($track_cites && $config['track_cites']) $tracked_cites[] = array($board['uri'], $post['id']); } } } - + // Cross-board linking if (preg_match_all('/(^|\s)>>>\/(' . $config['board_regex'] . 'f?)\/(\d+)?([\s,.)?]|$)/um', $body, $cites, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) { if (count($cites[0]) > $config['max_cites']) { error($config['error']['toomanycross']); } - + $skip_chars = 0; $body_tmp = $body; - + foreach ($cites as $matches) { $_board = $matches[2][0]; $cite = @$matches[3][0]; - + // preg_match_all is not multibyte-safe foreach ($matches as &$match) { $match[1] = mb_strlen(substr($body_tmp, 0, $match[1])); } - + // Temporarily store board information because it will be overwritten $tmp_board = $board['uri']; - + // Check if the board exists, and load settings if (openBoard($_board)) { if ($cite) { $query = prepare(sprintf("SELECT `thread`,`id` FROM ``posts_%s`` WHERE `id` = :id LIMIT 1", $board['uri'])); $query->bindValue(':id', $cite); $query->execute() or error(db_error($query)); - + if ($post = $query->fetch(PDO::FETCH_ASSOC)) { $replacement = '<a onclick="highlightReply(\''.$cite.'\');" href="' . $config['root'] . $board['dir'] . $config['dir']['res'] . ($post['thread']?$post['thread']:$post['id']) . '.html#' . $cite . '">' . '>>>/' . $_board . '/' . $cite . '</a>'; - + $body = mb_substr_replace($body, $matches[1][0] . $replacement . $matches[4][0], $matches[0][1] + $skip_chars, mb_strlen($matches[0][0])); $skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[4][0]) - mb_strlen($matches[0][0]); - + if ($track_cites && $config['track_cites']) $tracked_cites[] = array($board['uri'], $post['id']); } @@ -1616,19 +1649,19 @@ function markup(&$body, $track_cites = false) { $skip_chars += mb_strlen($matches[1][0] . $replacement . $matches[4][0]) - mb_strlen($matches[0][0]); } } - + // Restore main board settings openBoard($tmp_board); } } - + $body = preg_replace("/^\s*>.*$/m", '<span class="quote">$0</span>', $body); - + if ($config['strip_superfluous_returns']) $body = preg_replace('/\s+$/', '', $body); - + $body = preg_replace("/\n/", '<br/>', $body); - + return $tracked_cites; } @@ -1669,41 +1702,41 @@ function strip_combining_chars($str) { foreach ($chars as $char) { $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) continue; - + $str .= $char; } return $str; } function buildThread($id, $return = false, $mod = false) { - global $board, $config; + global $board, $config, $build_pages; $id = round($id); - + if (event('build-thread', $id)) return; - + if ($config['cache']['enabled'] && !$mod) { // Clear cache cache::delete("thread_index_{$board['uri']}_{$id}"); cache::delete("thread_{$board['uri']}_{$id}"); } - + $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)); - + while ($post = $query->fetch(PDO::FETCH_ASSOC)) { if (!isset($thread)) { $thread = new Thread( @@ -1719,11 +1752,11 @@ function buildThread($id, $return = false, $mod = false) { ); } } - + // Check if any posts were found if (!isset($thread)) error($config['error']['nonexistant']); - + $body = Element('thread.html', array( 'board' => $board, 'thread' => $thread, @@ -1733,9 +1766,12 @@ function buildThread($id, $return = false, $mod = false) { 'mod' => $mod, 'antibot' => $mod ? false : create_antibot($board['uri'], $id), 'boardlist' => createBoardlist($mod), - 'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['uri'] . '/' . $config['file_index']) + 'return' => ($mod ? '?' . $board['url'] . $config['file_index'] : $config['root'] . $board['dir'] . $config['file_index']) )); + if ($config['try_smarter'] && !$mod) + $build_pages[] = thread_find_page($id); + if ($return) return $body; @@ -1766,35 +1802,35 @@ function buildThread($id, $return = false, $mod = false) { function poster_id($ip, $thread) { global $config; - + if ($id = event('poster-id', $ip, $thread)) return $id; - + // Confusing, hard to brute-force, but simple algorithm return substr(sha1(sha1($ip . $config['secure_trip_salt'] . $thread) . $config['secure_trip_salt']), 0, $config['poster_id_length']); } function generate_tripcode($name) { global $config; - + if ($trip = event('tripcode', $name)) return $trip; - + if (!preg_match('/^([^#]+)?(##|#)(.+)$/', $name, $match)) return array($name); - + $name = $match[1]; $secure = $match[2] == '##'; $trip = $match[3]; - + // convert to SHIT_JIS encoding $trip = mb_convert_encoding($trip, 'Shift_JIS', 'UTF-8'); - + // generate salt $salt = substr($trip . 'H..', 1, 2); - $salt = preg_replace('/[^\.-z]/', '.', $salt); + $salt = preg_replace('/[^.-z]/', '.', $salt); $salt = strtr($salt, ':;<=>?@[\]^_`', 'ABCDEFGabcdef'); - + if ($secure) { if (isset($config['custom_tripcode']["##{$trip}"])) $trip = $config['custom_tripcode']["##{$trip}"]; @@ -1806,10 +1842,10 @@ function generate_tripcode($name) { else $trip = '!' . substr(crypt($trip, $salt), -10); } - + return array($name, $trip); } - + // Highest common factor function hcf($a, $b){ $gcd = 1; @@ -1835,7 +1871,7 @@ function fraction($numerator, $denominator, $sep) { $gcf = hcf($numerator, $denominator); $numerator = $numerator / $gcf; $denominator = $denominator / $gcf; - + return "{$numerator}{$sep}{$denominator}"; } @@ -1844,11 +1880,11 @@ function getPostByHash($hash) { $query = prepare(sprintf("SELECT `id`,`thread` FROM ``posts_%s`` WHERE `filehash` = :hash", $board['uri'])); $query->bindValue(':hash', $hash, PDO::PARAM_STR); $query->execute() or error(db_error($query)); - + if ($post = $query->fetch(PDO::FETCH_ASSOC)) { return $post; } - + return false; } @@ -1858,18 +1894,18 @@ function getPostByHashInThread($hash, $thread) { $query->bindValue(':hash', $hash, PDO::PARAM_STR); $query->bindValue(':thread', $thread, PDO::PARAM_INT); $query->execute() or error(db_error($query)); - + if ($post = $query->fetch(PDO::FETCH_ASSOC)) { return $post; } - + return false; } function undoImage(array $post) { if (!$post['has_file']) return; - + if (isset($post['file'])) file_unlink($post['file']); if (isset($post['thumb'])) @@ -1878,11 +1914,11 @@ function undoImage(array $post) { function rDNS($ip_addr) { global $config; - + if ($config['cache']['enabled'] && ($host = cache::get('rdns_' . $ip_addr))) { return $host; } - + if (!$config['dns_system']) { $host = gethostbyaddr($ip_addr); } else { @@ -1892,20 +1928,20 @@ function rDNS($ip_addr) { else $host = $ip_addr; } - + if ($config['cache']['enabled']) cache::set('rdns_' . $ip_addr, $host, 3600); - + return $host; } function DNS($host) { global $config; - + if ($config['cache']['enabled'] && ($ip_addr = cache::get('dns_' . $host))) { return $ip_addr; } - + if (!$config['dns_system']) { $ip_addr = gethostbyname($host); if ($ip_addr == $host) @@ -1917,9 +1953,9 @@ function DNS($host) { else $ip_addr = false; } - + if ($config['cache']['enabled']) cache::set('dns_' . $host, $ip_addr, 3600); - + return $ip_addr; } diff --git a/inc/image.php b/inc/image.php index 51db2f83..88f735dd 100644 --- a/inc/image.php +++ b/inc/image.php @@ -279,26 +279,24 @@ class ImageConvert extends ImageBase { } $this->temp = tempnam($config['tmp'], 'imagick'); - - $quality = $config['thumb_quality'] * 10; - - $config['thumb_keep_animation_frames'] = (int) $config['thumb_keep_animation_frames']; + + $config['thumb_keep_animation_frames'] = (int)$config['thumb_keep_animation_frames']; if ($this->format == 'gif' && ($config['thumb_ext'] == 'gif' || $config['thumb_ext'] == '') && $config['thumb_keep_animation_frames'] > 1) { if ($this->gifsicle) { - if (shell_exec("gifsicle --unoptimize -O2 --resize {$this->width}x{$this->height} < " . + if (trim($error = shell_exec("gifsicle --unoptimize -O2 --resize {$this->width}x{$this->height} < " . escapeshellarg($this->src . '') . " \"#0-{$config['thumb_keep_animation_frames']}\" > " . - escapeshellarg($this->temp)) || !file_exists($this->temp)) - error('Failed to resize image!'); + escapeshellarg($this->temp) . '2>&1 &&echo $?') !== '0') || !file_exists($this->temp)) + error('Failed to resize image!', null, $error); } else { - if (shell_exec("convert -background transparent -filter Point -sample {$this->width}x{$this->height} +antialias -quality {$quality} " . - escapeshellarg($this->src . '') . " " . escapeshellarg($this->temp)) || !file_exists($this->temp)) - error('Failed to resize image!'); + if (trim($error = shell_exec('convert ' . sprintf($config['convert_args'], '', $this->width, $this->height) . ' ' . + escapeshellarg($this->src) . ' ' . escapeshellarg($this->temp) . ' 2>&1 &&echo $?')) !== '0' || !file_exists($this->temp)) + error('Failed to resize image!', null, $error); } } else { - if (shell_exec("convert -background transparent -flatten -filter Point -scale {$this->width}x{$this->height} +antialias -quality {$quality} " . - escapeshellarg($this->src . '[0]') . " " . escapeshellarg($this->temp)) || !file_exists($this->temp)) - error('Failed to resize image!'); + if (trim($error = shell_exec('convert ' . sprintf($config['convert_args'], '-flatten', $this->width, $this->height) . ' ' . + escapeshellarg($this->src . '[0]') . " " . escapeshellarg($this->temp) . ' 2>&1 &&echo $?')) !== '0' || !file_exists($this->temp)) + error('Failed to resize image!', null, $error); } } } @@ -309,7 +307,7 @@ class ImagePNG extends ImageBase { } public function to($src) { global $config; - imagepng($this->image, $src, $config['thumb_quality']); + imagepng($this->image, $src); } public function resize() { $this->GD_create(); @@ -322,7 +320,7 @@ class ImagePNG extends ImageBase { class ImageGIF extends ImageBase { public function from() { - $this->image = @imagecreatefromgif ($this->src); + $this->image = @imagecreatefromgif($this->src); } public function to($src) { imagegif ($this->image, $src); diff --git a/inc/mod/config-editor.php b/inc/mod/config-editor.php index 484bd908..caca5a0e 100644 --- a/inc/mod/config-editor.php +++ b/inc/mod/config-editor.php @@ -13,13 +13,14 @@ function config_vars() { 'default_temp' => false ); $temp_comment = false; + $line_no = 0; foreach ($config_file as $line) { if ($temp_comment) { $var['comment'][] = $temp_comment; $temp_comment = false; } - if (preg_match('!^\s*// (.*)$!', $line, $matches)) { + if (preg_match('!^\s*// ([^$].*)$!', $line, $matches)) { if ($var['default'] !== false) { $line = ''; $temp_comment = $matches[1]; @@ -28,7 +29,10 @@ function config_vars() { } } else if ($var['default_temp'] !== false) { $var['default_temp'] .= "\n" . $line; - } elseif (preg_match('!^\s*\$config\[(.+?)\] = (.+?)(;( //.+)?)?$!', $line, $matches)) { + } elseif (preg_match('!^[\s/]*\$config\[(.+?)\] = (.+?)(;( //.+)?)?$!', $line, $matches)) { + if (preg_match('!^\s*//\s*!', $line)) { + $var['commented'] = true; + } $var['name'] = explode('][', $matches[1]); if (count($var['name']) == 1) { $var['name'] = preg_replace('/^\'(.*)\'$/', '$1', end($var['name'])); @@ -43,20 +47,37 @@ function config_vars() { $var['default_temp'] = $matches[2]; } - if (trim($line) === '') { - if ($var['name'] !== false) { - if ($var['default_temp']) - $var['default'] = $var['default_temp']; - - $temp = eval('return ' . $var['default'] . ';'); - if (!isset($temp)) + if ($var['name'] !== false) { + if ($var['default_temp']) + $var['default'] = $var['default_temp']; + if ($var['default'][0] == '&') + continue; // This is just an alias. + if (!preg_match('/^array|\[\]|function/', $var['default']) && !preg_match('/^Example: /', trim(implode(' ', $var['comment'])))) { + $syntax_error = true; + $temp = eval('$syntax_error = false;return ' . $var['default'] . ';'); + if ($syntax_error && $temp === false) { + error('Error parsing config.php (line ' . $line_no . ')!', null, $var); + } elseif (!isset($temp)) { $var['type'] = 'unknown'; - else + } else { $var['type'] = gettype($temp); + } - unset($var['default_temp']); + if ($var['type'] == 'integer' && $var['name'][0] == 'mod' && + (in_array($var['default'], array('JANITOR', 'MOD', 'ADMIN', 'DISABLED')) || mb_strpos($var['default'], "\$config['mod']") === 0)) { + // Permissions variable + $var['permissions'] = true; + } + unset($var['default_temp']); if (!is_array($var['name']) || (end($var['name']) != '' && !in_array(reset($var['name']), array('stylesheets')))) { + $already_exists = false; + foreach ($conf as $_var) { + if ($var['name'] == $_var['name']) + $already_exists = true; + + } + if (!$already_exists) $conf[] = $var; } } @@ -65,9 +86,17 @@ function config_vars() { 'name' => false, 'comment' => array(), 'default' => false, - 'default_temp' => false + 'default_temp' => false, + 'commented' => false, + 'permissions' => false, ); } + + if (trim($line) === '') { + $var['comment'] = array(); + } + + $line_no++; } return $conf; diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 615e85c7..0a9f825b 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -800,21 +800,21 @@ function mod_page_ip($ip) { $args['token'] = make_secure_link_token('ban'); if (hasPermission($config['mod']['view_ban'])) { - $query = prepare("SELECT ``bans``.*, `username` FROM ``bans`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `ip` = :ip"); + $query = prepare("SELECT ``bans``.*, `username` FROM ``bans`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `ip` = :ip ORDER BY `set` DESC"); $query->bindValue(':ip', $ip); $query->execute() or error(db_error($query)); $args['bans'] = $query->fetchAll(PDO::FETCH_ASSOC); } if (hasPermission($config['mod']['view_notes'])) { - $query = prepare("SELECT ``ip_notes``.*, `username` FROM ``ip_notes`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `ip` = :ip"); + $query = prepare("SELECT ``ip_notes``.*, `username` FROM ``ip_notes`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `ip` = :ip ORDER BY `time` DESC"); $query->bindValue(':ip', $ip); $query->execute() or error(db_error($query)); $args['notes'] = $query->fetchAll(PDO::FETCH_ASSOC); } if (hasPermission($config['mod']['modlog_ip'])) { - $query = prepare("SELECT `username`, `mod`, `ip`, `board`, `time`, `text` FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `text` LIKE :search ORDER BY `time` DESC LIMIT 20"); + $query = prepare("SELECT `username`, `mod`, `ip`, `board`, `time`, `text` FROM ``modlogs`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `text` LIKE :search ORDER BY `time` DESC LIMIT 50"); $query->bindValue(':search', '%' . $ip . '%'); $query->execute() or error(db_error($query)); $args['logs'] = $query->fetchAll(PDO::FETCH_ASSOC); @@ -1941,30 +1941,43 @@ function mod_report_dismiss($id, $all = false) { } -function mod_config() { - global $config, $mod; +function mod_config($board_config = false) { + global $config, $mod, $board; + + if ($board_config && !openBoard($board_config)) + error($config['error']['noboard']); - if (!hasPermission($config['mod']['edit_config'])) + if (!hasPermission($config['mod']['edit_config'], $board_config)) error($config['error']['noaccess']); + $config_file = $board_config ? $board['dir'] . 'config.php' : 'inc/instance-config.php'; + if ($config['mod']['config_editor_php']) { - $readonly = !is_writable('inc/instance-config.php'); + $readonly = !(is_file($config_file) ? is_writable($config_file) : is_writable(dirname($config_file))); if (!$readonly && isset($_POST['code'])) { $code = $_POST['code']; - file_put_contents('inc/instance-config.php', $code); - header('Location: ?/config', true, $config['redirect_http']); + file_put_contents($config_file, $code); + header('Location: ?/config' . ($board_config ? '/' . $board_config : ''), true, $config['redirect_http']); return; } - $instance_config = file_get_contents('inc/instance-config.php'); + $instance_config = @file_get_contents($config_file); + if ($instance_config === false) { + $instance_config = "<?php\n\n// This file does not exist yet. You are creating it."; + } $instance_config = str_replace("\n", '
', utf8tohtml($instance_config)); - mod_page(_('Config editor'), 'mod/config-editor-php.html', array('php' => $instance_config, 'readonly' => $readonly)); + mod_page(_('Config editor'), 'mod/config-editor-php.html', array( + 'php' => $instance_config, + 'readonly' => $readonly, + 'boards' => listBoards(), + 'board' => $board_config, + 'file' => $config_file + )); return; } - require_once 'inc/mod/config-editor.php'; $conf = config_vars(); @@ -1975,7 +1988,7 @@ function mod_config() { foreach ($var['name'] as $n) $c = &$c[$n]; } else { - $c = $config[$var['name']]; + $c = @$config[$var['name']]; } $var['value'] = $c; @@ -2010,14 +2023,28 @@ function mod_config() { $config_append .= '[' . var_export($var['name'], true) . ']'; } - $config_append .= ' = ' . var_export($value, true) . ";\n"; + + $config_append .= ' = '; + if (@$var['permissions'] && in_array($value, array(JANITOR, MOD, ADMIN, DISABLED))) { + $perm_array = array( + JANITOR => 'JANITOR', + MOD => 'MOD', + ADMIN => 'ADMIN', + DISABLED => 'DISABLED' + ); + $config_append .= $perm_array[$value]; + } else { + $config_append .= var_export($value, true); + } + $config_append .= ";\n"; } } if (!empty($config_append)) { $config_append = "\n// Changes made via web editor by \"" . $mod['username'] . "\" @ " . date('r') . ":\n" . $config_append . "\n"; - - if (!@file_put_contents('inc/instance-config.php', $config_append, FILE_APPEND)) { + if (!is_file($config_file)) + $config_append = "<?php\n\n$config_append"; + if (!@file_put_contents($config_file, $config_append, FILE_APPEND)) { $config_append = htmlentities($config_append); if ($config['minify_html']) @@ -2026,8 +2053,8 @@ function mod_config() { $page['title'] = 'Cannot write to file!'; $page['config'] = $config; $page['body'] = ' - <p style="text-align:center">Tinyboard could not write to <strong>inc/instance-config.php</strong> with the ammended configuration, probably due to a permissions error.</p> - <p style="text-align:center">You may proceed with these changes manually by copying and pasting the following code to the end of <strong>inc/instance-config.php</strong>:</p> + <p style="text-align:center">Tinyboard could not write to <strong>' . $config_file . '</strong> with the ammended configuration, probably due to a permissions error.</p> + <p style="text-align:center">You may proceed with these changes manually by copying and pasting the following code to the end of <strong>' . $config_file . '</strong>:</p> <textarea style="width:700px;height:370px;margin:auto;display:block;background:white;color:black" readonly>' . $config_append . '</textarea> '; echo Element('page.html', $page); @@ -2035,12 +2062,18 @@ function mod_config() { } } - header('Location: ?/', true, $config['redirect_http']); + header('Location: ?/config', true, $config['redirect_http']); exit; } - mod_page(_('Config editor'), 'mod/config-editor.html', array('conf' => $conf)); + mod_page(_('Config editor') . ($board_config ? ': ' . sprintf($config['board_abbreviation'], $board_config) : ''), + 'mod/config-editor.html', array( + 'boards' => listBoards(), + 'board' => $board_config, + 'conf' => $conf, + 'file' => $config_file + )); } function mod_themes_list() { diff --git a/install.php b/install.php index 70f27834..4b3785e0 100644 --- a/install.php +++ b/install.php @@ -459,7 +459,7 @@ if ($step == 0) { array( 'category' => 'Database', 'name' => 'MySQL PDO driver installed', - 'result' => extension_loaded('pdo') && in_array('mysql1', PDO::getAvailableDrivers()), + 'result' => extension_loaded('pdo') && in_array('mysql', PDO::getAvailableDrivers()), 'required' => true, 'message' => 'The required <a href="http://www.php.net/manual/en/ref.pdo-mysql.php">PDO MySQL driver</a> is not installed.', ), @@ -494,7 +494,7 @@ if ($step == 0) { array( 'category' => 'Image processing', 'name' => 'Imagick extension installed', - 'result' => extension_loaded('imagick1'), + 'result' => extension_loaded('imagick'), 'required' => false, 'message' => '(Optional) The PHP <a href="http://www.php.net/manual/en/imagick.installation.php">Imagick</a> (ImageMagick) extension is not installed. You may not use Imagick for better (and faster) image processing.', ), @@ -515,7 +515,7 @@ if ($step == 0) { array( 'category' => 'Image processing', 'name' => '`gifsicle` (command-line animted GIF thumbnailing)', - 'result' => $can_exec && shell_exec('which gifsicle1'), + 'result' => $can_exec && shell_exec('which gifsicle'), 'required' => false, 'message' => '(Optional) `gifsicle` was not found or executable; you may not use `convert+gifsicle` for better animated GIF thumbnailing.', ), @@ -532,6 +532,13 @@ if ($step == 0) { 'result' => is_writable('inc/instance-config.php'), 'required' => false, 'message' => 'Tinyboard does not have permission to make changes to inc/instance-config.php. To complete the installation, you will be asked to manually copy and paste code into the file instead.' + ), + array( + 'category' => 'Misc', + 'name' => 'Tinyboard installed using git', + 'result' => is_dir('.git.'), + 'required' => false, + 'message' => 'Tinyboard is still beta software and it\'s not going to come out of beta any time soon. As there are often many months between releases yet changes and bug fixes are very frequent, it\'s recommended to use the git repository to maintain your Tinyboard installation. Using git makes upgrading much easier.' ) ); @@ -550,167 +557,16 @@ if ($step == 0) { // Basic config $page['title'] = 'Configuration'; - function create_salt() { - return substr(base64_encode(sha1(rand())), 0, rand(25, 31)); - } - - $page['body'] = ' -<form action="?step=3" method="post"> - <fieldset> - <legend>Database</legend> - <label for="db_type">Type:</label> - <select id="db_type" name="db[type]">'; - - $drivers = PDO::getAvailableDrivers(); - - foreach ($drivers as &$driver) { - $driver_txt = $driver; - switch ($driver) { - case 'cubrid': - $driver_txt = 'Cubrid'; - break; - case 'dblib': - $driver_txt = 'FreeTDS / Microsoft SQL Server / Sybase'; - break; - case 'firebird': - $driver_txt = 'Firebird/Interbase 6'; - break; - case 'ibm': - $driver_txt = 'IBM DB2'; - break; - case 'informix': - $driver_txt = 'IBM Informix Dynamic Server'; - break; - case 'mysql': - $driver_txt = 'MySQL'; - break; - case 'oci': - $driver_txt = 'OCI'; - break; - case 'odbc': - $driver_txt = 'ODBC v3 (IBM DB2, unixODBC)'; - break; - case 'pgsql': - $driver_txt = 'PostgreSQL'; - break; - case 'sqlite': - $driver_txt = 'SQLite 3'; - break; - case 'sqlite2': - $driver_txt = 'SQLite 2'; - break; - } - $page['body'] .= '<option value="' . $driver . '">' . $driver_txt . '</option>'; - } - - $page['body'] .= ' - </select> - - <label for="db_server">Server:</label> - <input type="text" id="db_server" name="db[server]" value="localhost" /> - - <label for="db_db">Database:</label> - <input type="text" id="db_db" name="db[database]" value="" /> - - <label for="db_prefix">Table prefix (optional):</label> - <input type="text" id="db_prefix" name="db[prefix]" value="" /> - - <label for="db_user">Username:</label> - <input type="text" id="db_user" name="db[user]" value="" /> - - <label for="db_pass">Password:</label> - <input type="password" id="db_pass" name="db[password]" value="" /> - </fieldset> - <p style="text-align:center" class="unimportant">The following is all later configurable. For more options, <a href="http://tinyboard.org/docs/?p=Config">edit your configuration files</a> after installing.</p> - <fieldset> - <legend>Cookies</legend> - <label for="cookies_mod">Moderator cookie:</label> - <input type="text" id="cookies_mod" name="cookies[mod]" value="' . $config['cookies']['mod'] . '" /> - - <label for="cookies_salt">Secure salt:</label> - <input type="text" id="cookies_salt" name="cookies[salt]" value="' . create_salt() . '" size="40" /> - </fieldset> - - <fieldset> - <legend>Flood control</legend> - <label for="flood_time">Seconds before each post:</label> - <input type="text" id="flood_time" name="flood_time" value="' . $config['flood_time'] . '" /> - - <label for="flood_time_ip">Seconds before you can repost something (post the exact same text):</label> - <input type="text" id="flood_time_ip" name="flood_time_ip" value="' . $config['flood_time_ip'] . '" /> - - <label for="flood_time_same">Same as above, but with a different IP address:</label> - <input type="text" id="flood_time_same" name="flood_time_same" value="' . $config['flood_time_same'] . '" /> - - <label for="max_body">Maximum post body length:</label> - <input type="text" id="max_body" name="max_body" value="' . $config['max_body'] . '" /> - - <label for="reply_limit">Replies in a thread before it can no longer be bumped:</label> - <input type="text" id="reply_limit" name="reply_limit" value="' . $config['reply_limit'] . '" /> - - <label for="max_links">Maximum number of links in a single post:</label> - <input type="text" id="max_links" name="max_links" value="' . $config['max_links'] . '" /> - </fieldset> - - <fieldset> - <legend>Images</legend> - <label for="max_filesize">Maximum image filesize:</label> - <input type="text" id="max_filesize" name="max_filesize" value="' . $config['max_filesize'] . '" /> - - <label for="thumb_width">Thumbnail width:</label> - <input type="text" id="thumb_width" name="thumb_width" value="' . $config['thumb_width'] . '" /> - - <label for="thumb_height">Thumbnail height:</label> - <input type="text" id="thumb_height" name="thumb_height" value="' . $config['thumb_height'] . '" /> - - <label for="max_width">Maximum image width:</label> - <input type="text" id="max_width" name="max_width" value="' . $config['max_width'] . '" /> - - <label for="max_height">Maximum image height:</label> - <input type="text" id="max_height" name="max_height" value="' . $config['max_height'] . '" /> - </fieldset> - - <fieldset> - <legend>Display</legend> - <label for="threads_per_page">Threads per page:</label> - <input type="text" id="threads_per_page" name="threads_per_page" value="' . $config['threads_per_page'] . '" /> - - <label for="max_pages">Page limit:</label> - <input type="text" id="max_pages" name="max_pages" value="' . $config['max_pages'] . '" /> - - <label for="threads_preview">Number of replies to show per thread on the index page:</label> - <input type="text" id="threads_preview" name="threads_preview" value="' . $config['threads_preview'] . '" /> - </fieldset> + $config['cookies']['salt'] = substr(base64_encode(sha1(rand())), 0, 30); + $config['secure_trip_salt'] = substr(base64_encode(sha1(rand())), 0, 30); - <fieldset> - <legend>Directories</legend> - <label for="root">Root URI (include trailing slash):</label> - <input type="text" id="root" name="root" value="' . $config['root'] . '" /> - - <label for="dir_img">Image directory:</label> - <input type="text" id="dir_img" name="dir[img]" value="' . $config['dir']['img'] . '" /> - - <label for="dir_thumb">Thumbnail directory:</label> - <input type="text" id="dir_thumb" name="dir[thumb]" value="' . $config['dir']['thumb'] . '" /> - - <label for="dir_res">Thread directory:</label> - <input type="text" id="dir_res" name="dir[res]" value="' . $config['dir']['res'] . '" /> - </fieldset> - - <fieldset> - <legend>Miscellaneous</legend> - <label for="secure_trip_salt">Secure trip (##) salt:</label> - <input type="text" id="secure_trip_salt" name="secure_trip_salt" value="' . create_salt() . '" size="40" /> - </fieldset> - - <p style="text-align:center"> - <input type="submit" value="Complete installation" /> - </p> -</form> - '; - - - echo Element('page.html', $page); + echo Element('page.html', array( + 'body' => Element('installer/config.html', array( + 'config' => $config + )), + 'title' => 'Configuration', + 'config' => $config + )); } elseif ($step == 3) { $instance_config = '<?php @@ -771,7 +627,7 @@ if ($step == 0) { sql_open(); if (mysql_version() < 50503) - $sql = str_replace('utf8', 'utf8mb4', $sql); + $sql = preg_replace('/(CHARSET=|CHARACTER SET )utf8mb4/', '$1utf8', $sql); // This code is probably horrible, but what I'm trying // to do is find all of the SQL queires and put them @@ -21,64 +21,65 @@ if (get_magic_quotes_gpc()) { $query = isset($_SERVER['QUERY_STRING']) ? urldecode($_SERVER['QUERY_STRING']) : ''; $pages = array( - '' => ':?/', // redirect to dashboard - '/' => 'dashboard', // dashboard + '' => ':?/', // redirect to dashboard + '/' => 'dashboard', // dashboard '/confirm/(.+)' => 'confirm', // confirm action (if javascript didn't work) - '/logout' => 'logout', // logout + '/logout' => 'logout', // logout - '/users' => 'users', // manage users - '/users/(\d+)' => 'user', // edit user - '/users/(\d+)/(promote|demote)' => 'user_promote', // prmote/demote user + '/users' => 'users', // manage users + '/users/(\d+)' => 'user', // edit user + '/users/(\d+)/(promote|demote)' => 'user_promote', // prmote/demote user '/users/new' => 'user_new', // create a new user '/new_PM/([^/]+)' => 'new_pm', // create a new pm - '/PM/(\d+)(/reply)?' => 'pm', // read a pm - '/inbox' => 'inbox', // pm inbox + '/PM/(\d+)(/reply)?' => 'pm', // read a pm + '/inbox' => 'inbox', // pm inbox '/noticeboard' => 'noticeboard', // view noticeboard - '/noticeboard/(\d+)' => 'noticeboard', // view noticeboard - '/noticeboard/delete/(\d+)' => 'noticeboard_delete',// delete from noticeboard - '/log' => 'log', // modlog - '/log/(\d+)' => 'log', // modlog + '/noticeboard/(\d+)' => 'noticeboard', // view noticeboard + '/noticeboard/delete/(\d+)' => 'noticeboard_delete', // delete from noticeboard + '/log' => 'log', // modlog + '/log/(\d+)' => 'log', // modlog '/log:([^/]+)' => 'user_log', // modlog - '/log:([^/]+)/(\d+)' => 'user_log', // modlog - '/news' => 'news', // view news - '/news/(\d+)' => 'news', // view news - '/news/delete/(\d+)' => 'news_delete', // delete from news + '/log:([^/]+)/(\d+)' => 'user_log', // modlog + '/news' => 'news', // view news + '/news/(\d+)' => 'news', // view news + '/news/delete/(\d+)' => 'news_delete', // delete from news '/edit/(\%b)' => 'edit_board', // edit board details '/new-board' => 'new_board', // create a new board - '/rebuild' => 'rebuild', // rebuild static files - '/reports' => 'reports', // report queue - '/reports/(\d+)/dismiss(all)?' => 'report_dismiss', // dismiss a report + '/rebuild' => 'rebuild', // rebuild static files + '/reports' => 'reports', // report queue + '/reports/(\d+)/dismiss(all)?' => 'report_dismiss', // dismiss a report - '/IP/([\w.:]+)' => 'ip', // view ip address + '/IP/([\w.:]+)' => 'ip', // view ip address '/IP/([\w.:]+)/remove_note/(\d+)' => 'ip_remove_note', // remove note from ip address - '/bans' => 'bans', // ban list - '/bans/(\d+)' => 'bans', // ban list + '/bans' => 'bans', // ban list + '/bans/(\d+)' => 'bans', // ban list - '/search' => 'search_redirect', // search + '/search' => 'search_redirect', // search '/search/(posts|IP_notes|bans|log)/(.+)/(\d+)' => 'search', // search - '/search/(posts|IP_notes|bans|log)/(.+)' => 'search', // search + '/search/(posts|IP_notes|bans|log)/(.+)' => 'search', // search // CSRF-protected moderator actions - '/ban' => 'secure_POST ban', // new ban - '/(\%b)/ban(&delete)?/(\d+)' => 'secure_POST ban_post', // ban poster - '/(\%b)/move/(\d+)' => 'secure_POST move', // move thread + '/ban' => 'secure_POST ban', // new ban + '/(\%b)/ban(&delete)?/(\d+)' => 'secure_POST ban_post', // ban poster + '/(\%b)/move/(\d+)' => 'secure_POST move', // move thread '/(\%b)/edit(_raw)?/(\d+)' => 'secure_POST edit_post', // edit post - '/(\%b)/delete/(\d+)' => 'secure delete', // delete post - '/(\%b)/deletefile/(\d+)' => 'secure deletefile', // delete file from post + '/(\%b)/delete/(\d+)' => 'secure delete', // delete post + '/(\%b)/deletefile/(\d+)' => 'secure deletefile', // delete file from post '/(\%b)/deletebyip/(\d+)(/global)?' => 'secure deletebyip', // delete all posts by IP address - '/(\%b)/(un)?lock/(\d+)' => 'secure lock', // lock thread - '/(\%b)/(un)?sticky/(\d+)' => 'secure sticky', // sticky thread - '/(\%b)/bump(un)?lock/(\d+)' => 'secure bumplock', // "bumplock" thread + '/(\%b)/(un)?lock/(\d+)' => 'secure lock', // lock thread + '/(\%b)/(un)?sticky/(\d+)' => 'secure sticky', // sticky thread + '/(\%b)/bump(un)?lock/(\d+)' => 'secure bumplock', // "bumplock" thread - '/themes' => 'themes_list', // manage themes + '/themes' => 'themes_list', // manage themes '/themes/(\w+)' => 'theme_configure', // configure/reconfigure theme - '/themes/(\w+)/rebuild' => 'theme_rebuild', // rebuild theme - '/themes/(\w+)/uninstall' => 'theme_uninstall', // uninstall theme + '/themes/(\w+)/rebuild' => 'theme_rebuild', // rebuild theme + '/themes/(\w+)/uninstall' => 'theme_uninstall', // uninstall theme - '/config' => 'config', // config editor + '/config' => 'config', // config editor + '/config/(\%b)' => 'config', // config editor // these pages aren't listed in the dashboard without $config['debug'] '/debug/antispam' => 'debug_antispam', @@ -109,7 +110,7 @@ $new_pages = array(); foreach ($pages as $key => $callback) { if (is_string($callback) && preg_match('/^secure /', $callback)) $key .= '(/(?P<token>[a-f0-9]{8}))?'; - $key = str_replace('\%b', $config['board_regex'], $key); + $key = str_replace('\%b', '?P<board>' . sprintf(substr($config['board_path'], 0, -1), $config['board_regex']), $key); $new_pages[@$key[0] == '!' ? $key : '!^' . $key . '(?:&[^&=]+=[^&]*)*$!u'] = $callback; } $pages = $new_pages; @@ -118,6 +119,15 @@ foreach ($pages as $uri => $handler) { if (preg_match($uri, $query, $matches)) { $matches = array_slice($matches, 1); + if (isset($matches['board'])) { + $board_match = $matches['board']; + unset($matches['board']); + $key = array_search($board_match, $matches); + if (preg_match('/^' . sprintf(substr($config['board_path'], 0, -1), '(' . $config['board_regex'] . ')') . '$/u', $matches[$key], $board_match)) { + $matches[$key] = $board_match[1]; + } + } + if (is_string($handler) && preg_match('/^secure(_POST)? /', $handler, $m)) { $secure_post_only = isset($m[1]); if (!$secure_post_only || $_SERVER['REQUEST_METHOD'] == 'POST') { @@ -218,7 +218,7 @@ if (isset($_POST['delete'])) { } if (!$post['mod']) { - $post['antispam_hash'] = checkSpam(array($board['uri'], isset($post['thread']) && !($config['quick_reply'] && isset($_POST['quick-reply'])) ? $post['thread'] : null)); + $post['antispam_hash'] = checkSpam(array($board['uri'], isset($post['thread']) && !($config['quick_reply'] && isset($_POST['quick-reply'])) ? $post['thread'] : ($config['try_smarter'] && isset($_POST['page']) ? 0 - (int)$_POST['page'] : null))); if ($post['antispam_hash'] === true) error($config['error']['spam']); } @@ -430,7 +430,7 @@ if (isset($_POST['delete'])) { $post['filehash'] = $config['file_hash']($upload); $post['filesize'] = filesize($upload); - if ($is_an_image) { + if ($is_an_image && $config['ie_mime_type_detection'] !== false) { // Check IE MIME type detection XSS exploit $buffer = file_get_contents($upload, null, null, null, 255); if (preg_match($config['ie_mime_type_detection'], $buffer)) { @@ -596,12 +596,15 @@ if (isset($_POST['delete'])) { } } - buildThread($post['op'] ? $id : $post['thread']); - if (!$post['op'] && strtolower($post['email']) != 'sage' && !$thread['sage'] && ($config['reply_limit'] == 0 || $numposts['replies']+1 < $config['reply_limit'])) { bumpThread($post['thread']); } + buildThread($post['op'] ? $id : $post['thread']); + + if ($config['try_smarter'] && $post['op']) + $build_pages = range(1, $config['max_pages']); + if ($post['op']) clean(); diff --git a/stylesheets/gentoochan.css b/stylesheets/gentoochan.css index 8213eb26..eb3d6471 100644 --- a/stylesheets/gentoochan.css +++ b/stylesheets/gentoochan.css @@ -19,9 +19,12 @@ a.post_no { color: #ccc; } div.post.reply, input, textarea { - background: rgba(0, 0, 0, 0.1)!important; - border: 1px solid rgba(0, 0, 0, 0.2)!important; - border-radius: 2px !important; + background: rgba(0, 0, 0, 0.1); + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 2px; +} +div.post.reply.post-hover { + background: rgba(200, 200, 200, 0.85); } div.post.reply.highlighted { background: #f0c0b0; diff --git a/stylesheets/ricechan.css b/stylesheets/ricechan.css index b8549726..c6ba19b6 100644 --- a/stylesheets/ricechan.css +++ b/stylesheets/ricechan.css @@ -95,7 +95,7 @@ form table tr th { background: #7E3737 !important; color: #B99B9B !important; } -input[type="text"], input[type="password"], textarea { +input[type="text"], input[type="password"], textarea, select { border: 2px solid rgba(94, 50, 50, 0.56) !important; background: #7C7373 !important; color: #311313 !important; diff --git a/templates/error.html b/templates/error.html new file mode 100644 index 00000000..3b8eb693 --- /dev/null +++ b/templates/error.html @@ -0,0 +1,18 @@ +<div style="text-align:center"> + <h2 style="margin:20px 0">{{ message }}</h2> + {% if board %} + <p> + <a href="{{ config.root }}{% if mod %}{{ config.file_mod }}?/{% endif %}{{ board.dir }}{{ config.file_index }}"> + {% trans 'Go back' %} + </a> + </p> + {% endif %} +</div> +{% if debug and config.debug %} + <hr> + <h3>{% trans 'Error information' %}</h3> + <pre style="white-space:pre-wrap;font-size: 10px;"> + {{ debug }} + </pre> + <hr> +{% endif %}
\ No newline at end of file diff --git a/templates/generic_page.html b/templates/generic_page.html index 41300f6b..70cb15aa 100644 --- a/templates/generic_page.html +++ b/templates/generic_page.html @@ -22,7 +22,8 @@ {% include 'attention_bar.html' %} {% include 'post_form.html' %} - {% if config.blotter %}<hr /><div class="blotter">{{ config.blotter }}</div>{% endif %} + + {% if config.global_message %}<hr /><div class="blotter">{{ config.global_message }}</div>{% endif %} <hr /> <form name="postcontrols" action="{{ config.post_url }}" method="post"> <input type="hidden" name="board" value="{{ board.uri }}" /> diff --git a/templates/index.html b/templates/index.html index f01d6851..12150d0b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -38,7 +38,7 @@ {% include 'boardlist.html' %} {% endif %} - {% if config.blotter %}<hr /><div class="blotter">{{ config.blotter }}</div>{% endif %} + {% if config.global_message %}<hr /><div class="blotter">{{ config.global_message }}</div>{% endif %} <hr /> <form name="postcontrols" action="{{ config.post_url }}" method="post"> <input type="hidden" name="board" value="{{ board.uri }}" /> diff --git a/templates/installer/check-requirements.html b/templates/installer/check-requirements.html index fea6fed8..b6003f3a 100644 --- a/templates/installer/check-requirements.html +++ b/templates/installer/check-requirements.html @@ -14,14 +14,14 @@ <td>{{ test.name }}</td> <td class="minimal" style="text-align:center"> {% if test.result %} - <i style="color:#090" class="icon-check-sign"></i> + <i style="font-size:11pt;color:#090" class="icon-check-sign"></i> {% else %} {% if test.required %} {% set errors = errors + 1 %} - <i style="color:#d00" class="icon-exclamation-sign"></i> + <i style="font-size:11pt;color:#d00" class="icon-exclamation-sign"></i> {% else %} {% set warnings = warnings + 1 %} - <i style="color:#f80" class="icon-warning-sign"></i> + <i style="font-size:11pt;color:#f80" class="icon-warning-sign"></i> {% endif %} {% endif %} </td> @@ -34,9 +34,9 @@ {% for test in tests if not test.result%} <li style="margin-bottom:5px"> {% if test.required %} - <i style="color:#d00" class="icon-exclamation-sign"></i> <strong>Error:</strong> + <i style="font-size:11pt;color:#d00" class="icon-exclamation-sign"></i> <strong>Error:</strong> {% else %} - <i style="color:#f80" class="icon-warning-sign"></i> <strong>Warning:</strong> + <i style="font-size:11pt;color:#f80" class="icon-warning-sign"></i> <strong>Warning:</strong> {% endif %} {{ test.message }} </li> diff --git a/templates/installer/config.html b/templates/installer/config.html new file mode 100644 index 00000000..193bb98b --- /dev/null +++ b/templates/installer/config.html @@ -0,0 +1,95 @@ +<form action="?step=3" method="post" autocomplete="off"> + <fieldset> + <legend>Database (MySQL)</legend> + <label for="db_server">Server:</label> + <input type="text" id="db_server" name="db[server]" value="{{ config.db.server }}"> + + <label for="db_db">Database:</label> + <input type="text" id="db_db" name="db[database]" value="{{ config.db.database }}"> + + <label for="db_prefix">Table prefix (optional):</label> + <input type="text" id="db_prefix" name="db[prefix]" value="{{ config.db.prefix }}"> + + <label for="db_user">Username:</label> + <input type="text" id="db_user" name="db[user]" value="{{ config.db.user }}"> + + <label for="db_pass">Password:</label> + <input type="password" id="db_pass" name="db[password]" value=""> + </fieldset> + <p style="text-align:center" class="unimportant">The following is all later configurable. For more options, <a href="http://tinyboard.org/docs/?p=Config">edit your configuration files</a> after installing.</p> + <fieldset> + <legend>Cookies</legend> + <label for="cookies_mod">Moderator cookie:</label> + <input type="text" id="cookies_mod" name="cookies[mod]" value="{{ config.cookies.mod }}"> + + <label for="cookies_salt">Secure salt:</label> + <input type="text" id="cookies_salt" name="cookies[salt]" value="{{ config.cookies.salt }}" size="40"> + </fieldset> + + <fieldset> + <legend>Flood control</legend> + <label for="flood_time">Seconds before each post:</label> + <input type="text" id="flood_time" name="flood_time" value="{{ config.flood_time }}"> + + <label for="flood_time_ip">Seconds before you can repost something (post the exact same text):</label> + <input type="text" id="flood_time_ip" name="flood_time_ip" value="{{ config.flood_time_ip }}"> + + <label for="flood_time_same">Same as above, but with a different IP address:</label> + <input type="text" id="flood_time_same" name="flood_time_same" value="{{ config.flood_time_same }}"> + + <label for="max_body">Maximum post body length:</label> + <input type="text" id="max_body" name="max_body" value="{{ config.max_body }}"> + + <label for="reply_limit">Replies in a thread before it can no longer be bumped:</label> + <input type="text" id="reply_limit" name="reply_limit" value="{{ config.reply_limit }}"> + + <label for="max_links">Maximum number of links in a single post:</label> + <input type="text" id="max_links" name="max_links" value="{{ config.max_links }}"> + </fieldset> + + <fieldset> + <legend>Images</legend> + <label for="max_filesize">Maximum image filesize (bytes):</label> + <input type="text" id="max_filesize" name="max_filesize" value="{{ config.max_filesize }}"> + + <label for="thumb_width">Thumbnail width:</label> + <input type="text" id="thumb_width" name="thumb_width" value="{{ config.thumb_width }}"> + + <label for="thumb_height">Thumbnail height:</label> + <input type="text" id="thumb_height" name="thumb_height" value="{{ config.thumb_height }}"> + + <label for="max_width">Maximum image width:</label> + <input type="text" id="max_width" name="max_width" value="{{ config.max_width }}"> + + <label for="max_height">Maximum image height:</label> + <input type="text" id="max_height" name="max_height" value="{{ config.max_height }}"> + </fieldset> + + <fieldset> + <legend>Display</legend> + <label for="threads_per_page">Threads per page:</label> + <input type="text" id="threads_per_page" name="threads_per_page" value="{{ config.threads_per_page }}"> + + <label for="max_pages">Page limit:</label> + <input type="text" id="max_pages" name="max_pages" value="{{ config.max_pages }}"> + + <label for="threads_preview">Number of replies to show per thread on the index page:</label> + <input type="text" id="threads_preview" name="threads_preview" value="{{ config.threads_preview }}"> + </fieldset> + + <fieldset> + <legend>Directories</legend> + <label for="root">Root URI (include trailing slash):</label> + <input type="text" id="root" name="root" value="{{ config.root }}" size="40"> + </fieldset> + + <fieldset> + <legend>Miscellaneous</legend> + <label for="secure_trip_salt">Secure trip (##) salt:</label> + <input type="text" id="secure_trip_salt" name="secure_trip_salt" value="{{ config.secure_trip_salt }}" size="40"> + </fieldset> + + <p style="text-align:center"> + <input type="submit" value="Complete installation"> + </p> +</form> diff --git a/templates/mod/config-editor-php.html b/templates/mod/config-editor-php.html index 54c398e4..4f76b5dd 100644 --- a/templates/mod/config-editor-php.html +++ b/templates/mod/config-editor-php.html @@ -1,17 +1,35 @@ -{% if readonly %} - <p style="text-align:center;max-width:800px;margin:20px auto">Tinyboard does not have the required permissions to edit <strong>inc/instance-config.php</strong>. To make changes, you will need to change the file's permissions first or manually edit the code.</p> -{% endif %} - - -{% if not readonly %}<form method="post" action="">{% endif %} - <textarea name="code" id="code" style="display:block; margin:auto;width:100%;max-width:800px;height:500px{% if readonly %};background:#eee" readonly{% else %}"{% endif %}> - {{ php }} - </textarea> +<div style="max-width:800px;margin:auto"> + <p> + Any changes you make here will simply be appended to <code>{{ file }}</code>. If you wish to make the most of Tinyboard's customizability, you can instead edit the file directly. This page is intended for making quick changes and for those who don't have a basic understanding of PHP code. + </p> + {% if boards|count %} + <ul> + {% if board %} + <li><a href="?/config">Edit site-wide config</a></li> + {% endif %} + {% for _board in boards if _board.uri != board %} + <li> + <a href="?/config/{{ _board.uri }}">Edit config for {{ config.board_abbreviation|sprintf(_board.uri) }}</a> + </li> + {% endfor %} + </ul> + {% endif %} + + {% if readonly %} + <p>Tinyboard does not have the required permissions to edit <code>{{ file }}</code>. To make changes, you will need to change the file's permissions first or manually edit the code.</p> + {% endif %} + - <ul style="padding:0;text-align:center;list-style:none"> - <li><input name="save" type="submit" value="{% trans 'Save changes' %}"{% if readonly %} disabled{% endif %}></li> - </ul> -{% if not readonly %}</form>{% endif %} + {% if not readonly %}<form method="post" action="">{% endif %} + <textarea name="code" id="code" style="margin:auto;width:100%;height:500px{% if readonly %};background:#eee" readonly{% else %}"{% endif %}> + {{ php }} + </textarea> + + <ul style="padding:0;text-align:center;list-style:none"> + <li><input name="save" type="submit" value="{% trans 'Save changes' %}"{% if readonly %} disabled{% endif %}></li> + </ul> + {% if not readonly %}</form>{% endif %} +</div> <script type="text/javascript"> var observe; diff --git a/templates/mod/config-editor.html b/templates/mod/config-editor.html index 41e01dad..a43f8cf7 100644 --- a/templates/mod/config-editor.html +++ b/templates/mod/config-editor.html @@ -1,10 +1,25 @@ +<p> + Any changes you make here will simply be appended to <code>{{ file }}</code>. If you wish to make the most of Tinyboard's customizability, you can instead edit the file directly. This page is intended for making quick changes and for those who don't have a basic understanding of PHP code. +</p> +{% if boards|count %} + <ul> + {% if board %} + <li><a href="?/config">Edit site-wide config</a></li> + {% endif %} + {% for _board in boards if _board.uri != board %} + <li> + <a href="?/config/{{ _board.uri }}">Edit config for {{ config.board_abbreviation|sprintf(_board.uri) }}</a> + </li> + {% endfor %} + </ul> +{% endif %} <form method="post" action=""> <table class="mod config-editor"> <tr> - <th class="minimal">Name</th> - <th>Value</th> - <th class="minimal">Type</th> - <th>Description</th> + <th class="minimal">{% trans 'Name' %}</th> + <th>{% trans 'Value' %}</th> + <th class="minimal">{% trans 'Type' %}</th> + <th>{% trans 'Description' %}</th> </tr> {% for var in conf if var.type != 'array' %} {% if var.name|count == 1 %} @@ -25,7 +40,7 @@ <td> {% if var.type == 'string' %} <input name="{{ name }}" type="text" value="{{ var.value|e }}"> - {% elseif var.type == 'integer' and var.name.0 == 'mod' and (var.default in ['JANITOR', 'MOD', 'ADMIN', 'DISABLED'] or var.default|slice(0, 14) == "$config['mod']") and var.value <= constant('DISABLED') %} + {% elseif var.permissions %} <select name="{{ name }}"> <option value="{{ constant('JANITOR') }}"{% if var.value == constant('JANITOR')%} selected{% endif %}>JANITOR</option> <option value="{{ constant('MOD') }}"{% if var.value == constant('MOD')%} selected{% endif %}>MOD</option> @@ -49,8 +64,8 @@ {{ var.type|e }} </td> - <td> - {{ var.comment|join('<br>') }} + <td style="word-wrap:break-word;width:50%"> + {{ var.comment|join(' ') }} </td> </tr> {% endfor %} diff --git a/templates/post_form.html b/templates/post_form.html index 391f0657..12f1186b 100644 --- a/templates/post_form.html +++ b/templates/post_form.html @@ -4,6 +4,9 @@ {{ antibot.html() }} <input type="hidden" name="board" value="{{ board.uri }}"> {{ antibot.html() }} +{% if current_page %} + <input type="hidden" name="page" value="{{ current_page }}"> +{% endif %} {% if mod %}<input type="hidden" name="mod" value="1">{% endif %} <table> {% if not config.field_disable_name or (mod and post.mod|hasPermission(config.mod.bypass_field_disable, board.uri)) %}<tr> diff --git a/templates/thread.html b/templates/thread.html index 38be37ab..7fe60a39 100644 --- a/templates/thread.html +++ b/templates/thread.html @@ -30,7 +30,7 @@ {% include 'post_form.html' %} - {% if config.blotter %}<hr /><div class="blotter">{{ config.blotter }}</div>{% endif %} + {% if config.global_message %}<hr /><div class="blotter">{{ config.global_message }}</div>{% endif %} <hr /> <form name="postcontrols" action="{{ config.post_url }}" method="post"> <input type="hidden" name="board" value="{{ board.uri }}" /> |
