Conflicts: inc/config.php inc/display.php inc/mod/pages.php install.php js/quick-reply.js post.php templates/index.htmltags/vichan-devel-4.4.92
@@ -30,7 +30,7 @@ it need one. | |||
### Recommended | |||
1. PHP >= 5.3 | |||
2. MySQL server >= 5.5.3 | |||
3. ImageMagick or command-line version (```convert``` and ```identify```) | |||
3. ImageMagick (command-line ImageMagick or GraphicsMagick preferred). | |||
4. [APC (Alternative PHP Cache)](http://php.net/manual/en/book.apc.php), [XCache](http://xcache.lighttpd.net/) or [Memcached](http://www.php.net/manual/en/intro.memcached.php) | |||
Contributing | |||
@@ -4,24 +4,26 @@ | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { | |||
// You cannot request this file directly. | |||
exit; | |||
} | |||
defined('TINYBOARD') or exit; | |||
$hidden_inputs_twig = array(); | |||
class AntiBot { | |||
public $salt, $inputs = array(), $index = 0; | |||
public static function randomString($length, $uppercase = false, $special_chars = false) { | |||
public static function randomString($length, $uppercase = false, $special_chars = false, $unicode_chars = false) { | |||
$chars = 'abcdefghijklmnopqrstuvwxyz0123456789'; | |||
if ($uppercase) | |||
$chars .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; | |||
if ($special_chars) | |||
$chars .= ' ~!@#$%^&*()_+,./;\'[]\\{}|:<>?=-` '; | |||
if ($unicode_chars) { | |||
$len = strlen($chars) / 10; | |||
for ($n = 0; $n < $len; $n++) | |||
$chars .= mb_convert_encoding('&#' . mt_rand(0x2600, 0x26FF) . ';', 'UTF-8', 'HTML-ENTITIES'); | |||
} | |||
$chars = str_split($chars); | |||
$chars = preg_split('//u', $chars, -1, PREG_SPLIT_NO_EMPTY); | |||
$ch = array(); | |||
@@ -44,10 +46,10 @@ class AntiBot { | |||
} | |||
public static function make_confusing($string) { | |||
$chars = str_split($string); | |||
$chars = preg_split('//u', $string, -1, PREG_SPLIT_NO_EMPTY); | |||
foreach ($chars as &$c) { | |||
if (rand(0, 2) != 0) | |||
if (mt_rand(0, 3) != 0) | |||
$c = utf8tohtml($c); | |||
else | |||
$c = mb_encode_numericentity($c, array(0, 0xffff, 0, 0xffff), 'UTF-8'); | |||
@@ -68,13 +70,13 @@ class AntiBot { | |||
shuffle($config['spam']['hidden_input_names']); | |||
$input_count = rand($config['spam']['hidden_inputs_min'], $config['spam']['hidden_inputs_max']); | |||
$input_count = mt_rand($config['spam']['hidden_inputs_min'], $config['spam']['hidden_inputs_max']); | |||
$hidden_input_names_x = 0; | |||
for ($x = 0; $x < $input_count ; $x++) { | |||
if ($hidden_input_names_x === false || rand(0, 2) == 0) { | |||
if ($hidden_input_names_x === false || mt_rand(0, 2) == 0) { | |||
// Use an obscure name | |||
$name = $this->randomString(rand(10, 40)); | |||
$name = $this->randomString(mt_rand(10, 40), false, false, $config['spam']['unicode']); | |||
} else { | |||
// Use a pre-defined confusing name | |||
$name = $config['spam']['hidden_input_names'][$hidden_input_names_x++]; | |||
@@ -82,25 +84,33 @@ class AntiBot { | |||
$hidden_input_names_x = false; | |||
} | |||
if (rand(0, 2) == 0) { | |||
if (mt_rand(0, 2) == 0) { | |||
// Value must be null | |||
$this->inputs[$name] = ''; | |||
} elseif (rand(0, 4) == 0) { | |||
} elseif (mt_rand(0, 4) == 0) { | |||
// Numeric value | |||
$this->inputs[$name] = (string)rand(0, 100); | |||
$this->inputs[$name] = (string)mt_rand(0, 100000); | |||
} else { | |||
// Obscure value | |||
$this->inputs[$name] = $this->randomString(rand(5, 100), true, true); | |||
$this->inputs[$name] = $this->randomString(mt_rand(5, 100), true, true, $config['spam']['unicode']); | |||
} | |||
} | |||
} | |||
public static function space() { | |||
if (mt_rand(0, 3) != 0) | |||
return ' '; | |||
return str_repeat(' ', mt_rand(1, 3)); | |||
} | |||
public function html($count = false) { | |||
global $config; | |||
$elements = array( | |||
'<input type="hidden" name="%name%" value="%value%">', | |||
'<input type="hidden" value="%value%" name="%name%">', | |||
'<input name="%name%" value="%value%" type="hidden">', | |||
'<input value="%value%" name="%name%" type="hidden">', | |||
'<input style="display:none" type="text" name="%name%" value="%value%">', | |||
'<input style="display:none" type="text" value="%value%" name="%name%">', | |||
'<span style="display:none"><input type="text" name="%name%" value="%value%"></span>', | |||
@@ -113,7 +123,7 @@ class AntiBot { | |||
$html = ''; | |||
if ($count === false) { | |||
$count = rand(1, count($this->inputs) / 15); | |||
$count = mt_rand(1, abs(count($this->inputs) / 15) + 1); | |||
} | |||
if ($count === true) { | |||
@@ -128,6 +138,9 @@ class AntiBot { | |||
$element = false; | |||
while (!$element) { | |||
$element = $elements[array_rand($elements)]; | |||
$element = str_replace(' ', self::space(), $element); | |||
if (mt_rand(0, 5) == 0) | |||
$element = str_replace('>', self::space() . '>', $element); | |||
if (strpos($element, 'textarea') !== false && $value == '') { | |||
// There have been some issues with mobile web browsers and empty <textarea>'s. | |||
$element = false; | |||
@@ -136,7 +149,7 @@ class AntiBot { | |||
$element = str_replace('%name%', utf8tohtml($name), $element); | |||
if (rand(0, 2) == 0) | |||
if (mt_rand(0, 2) == 0) | |||
$value = $this->make_confusing($value); | |||
else | |||
$value = utf8tohtml($value); | |||
@@ -3,6 +3,7 @@ | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
defined('TINYBOARD') or exit; | |||
/** | |||
* Class for generating json API compatible with 4chan API | |||
@@ -0,0 +1,258 @@ | |||
<?php | |||
require 'inc/lib/IP/Lifo/IP/IP.php'; | |||
require 'inc/lib/IP/Lifo/IP/BC.php'; | |||
require 'inc/lib/IP/Lifo/IP/CIDR.php'; | |||
use Lifo\IP\CIDR; | |||
class Bans { | |||
static public function range_to_string($mask) { | |||
list($ipstart, $ipend) = $mask; | |||
if (!isset($ipend) || $ipend === false) { | |||
// Not a range. Single IP address. | |||
$ipstr = inet_ntop($ipstart); | |||
return $ipstr; | |||
} | |||
if (strlen($ipstart) != strlen($ipend)) | |||
return '???'; // What the fuck are you doing, son? | |||
$range = CIDR::range_to_cidr(inet_ntop($ipstart), inet_ntop($ipend)); | |||
if ($range !== false) | |||
return $range; | |||
return '???'; | |||
} | |||
private static function calc_cidr($mask) { | |||
$cidr = new CIDR($mask); | |||
$range = $cidr->getRange(); | |||
return array(inet_pton($range[0]), inet_pton($range[1])); | |||
} | |||
private static function parse_time($str) { | |||
if (empty($str)) | |||
return false; | |||
if (($time = @strtotime($str)) !== false) | |||
return $time; | |||
if (!preg_match('/^((\d+)\s?ye?a?r?s?)?\s?+((\d+)\s?mon?t?h?s?)?\s?+((\d+)\s?we?e?k?s?)?\s?+((\d+)\s?da?y?s?)?((\d+)\s?ho?u?r?s?)?\s?+((\d+)\s?mi?n?u?t?e?s?)?\s?+((\d+)\s?se?c?o?n?d?s?)?$/', $str, $matches)) | |||
return false; | |||
$expire = 0; | |||
if (isset($matches[2])) { | |||
// Years | |||
$expire += $matches[2]*60*60*24*365; | |||
} | |||
if (isset($matches[4])) { | |||
// Months | |||
$expire += $matches[4]*60*60*24*30; | |||
} | |||
if (isset($matches[6])) { | |||
// Weeks | |||
$expire += $matches[6]*60*60*24*7; | |||
} | |||
if (isset($matches[8])) { | |||
// Days | |||
$expire += $matches[8]*60*60*24; | |||
} | |||
if (isset($matches[10])) { | |||
// Hours | |||
$expire += $matches[10]*60*60; | |||
} | |||
if (isset($matches[12])) { | |||
// Minutes | |||
$expire += $matches[12]*60; | |||
} | |||
if (isset($matches[14])) { | |||
// Seconds | |||
$expire += $matches[14]; | |||
} | |||
return time() + $expire; | |||
} | |||
static public function parse_range($mask) { | |||
$ipstart = false; | |||
$ipend = false; | |||
if (preg_match('@^(\d{1,3}\.){1,3}([\d*]{1,3})?$@', $mask) && substr_count($mask, '*') == 1) { | |||
// IPv4 wildcard mask | |||
$parts = explode('.', $mask); | |||
$ipv4 = ''; | |||
foreach ($parts as $part) { | |||
if ($part == '*') { | |||
$ipstart = inet_pton($ipv4 . '0' . str_repeat('.0', 3 - substr_count($ipv4, '.'))); | |||
$ipend = inet_pton($ipv4 . '255' . str_repeat('.255', 3 - substr_count($ipv4, '.'))); | |||
break; | |||
} elseif(($wc = strpos($part, '*')) !== false) { | |||
$ipstart = inet_pton($ipv4 . substr($part, 0, $wc) . '0' . str_repeat('.0', 3 - substr_count($ipv4, '.'))); | |||
$ipend = inet_pton($ipv4 . substr($part, 0, $wc) . '9' . str_repeat('.255', 3 - substr_count($ipv4, '.'))); | |||
break; | |||
} | |||
$ipv4 .= "$part."; | |||
} | |||
} elseif (preg_match('@^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d+$@', $mask)) { | |||
list($ipv4, $bits) = explode('/', $mask); | |||
if ($bits > 32) | |||
return false; | |||
list($ipstart, $ipend) = self::calc_cidr($mask); | |||
} elseif (preg_match('@^[:a-z\d]+/\d+$@i', $mask)) { | |||
list($ipv6, $bits) = explode('/', $mask); | |||
if ($bits > 128) | |||
return false; | |||
list($ipstart, $ipend) = self::calc_cidr($mask); | |||
} else { | |||
if (($ipstart = @inet_pton($mask)) === false) | |||
return false; | |||
} | |||
return array($ipstart, $ipend); | |||
} | |||
static public function find($ip, $board = false, $get_mod_info = false) { | |||
global $config; | |||
$query = prepare('SELECT ``bans``.*' . ($get_mod_info ? ', `username`' : '') . ' FROM ``bans`` | |||
' . ($get_mod_info ? 'LEFT JOIN ``mods`` ON ``mods``.`id` = `creator`' : '') . ' | |||
WHERE | |||
(' . ($board ? '(`board` IS NULL OR `board` = :board) AND' : '') . ' | |||
(`ipstart` = :ip OR (:ip >= `ipstart` AND :ip <= `ipend`))) | |||
ORDER BY `expires` IS NULL, `expires` DESC'); | |||
if ($board) | |||
$query->bindValue(':board', $board); | |||
$query->bindValue(':ip', inet_pton($ip)); | |||
$query->execute() or error(db_error($query)); | |||
$ban_list = array(); | |||
while ($ban = $query->fetch(PDO::FETCH_ASSOC)) { | |||
if ($ban['expires'] && ($ban['seen'] || !$config['require_ban_view']) && $ban['expires'] < time()) { | |||
$query = prepare("DELETE FROM ``bans`` WHERE `id` = :id"); | |||
$query->bindValue(':id', $ban['id'], PDO::PARAM_INT); | |||
$query->execute() or error(db_error($query)); | |||
} else { | |||
$ban['mask'] = self::range_to_string(array($ban['ipstart'], $ban['ipend'])); | |||
$ban_list[] = $ban; | |||
} | |||
} | |||
return $ban_list; | |||
} | |||
static public function list_all($offset = 0, $limit = 9001) { | |||
$offset = (int)$offset; | |||
$limit = (int)$limit; | |||
$query = query("SELECT ``bans``.*, `username` FROM ``bans`` | |||
LEFT JOIN ``mods`` ON ``mods``.`id` = `creator` | |||
ORDER BY `created` DESC LIMIT $offset, $limit") or error(db_error()); | |||
$bans = $query->fetchAll(PDO::FETCH_ASSOC); | |||
foreach ($bans as &$ban) { | |||
$ban['mask'] = self::range_to_string(array($ban['ipstart'], $ban['ipend'])); | |||
} | |||
return $bans; | |||
} | |||
static public function count() { | |||
$query = query("SELECT COUNT(*) FROM ``bans``") or error(db_error()); | |||
return (int)$query->fetchColumn(); | |||
} | |||
static public function seen($ban_id) { | |||
$query = query("UPDATE ``bans`` SET `seen` = 1 WHERE `id` = " . (int)$ban_id) or error(db_error()); | |||
} | |||
static public function purge() { | |||
$query = query("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` < " . time() . " AND `seen` = 1") or error(db_error()); | |||
} | |||
static public function delete($ban_id, $modlog = false) { | |||
if ($modlog) { | |||
$query = query("SELECT `ipstart`, `ipend` FROM ``bans`` WHERE `id` = " . (int)$ban_id) or error(db_error()); | |||
if (!$ban = $query->fetch(PDO::FETCH_ASSOC)) { | |||
// Ban doesn't exist | |||
return false; | |||
} | |||
$mask = self::range_to_string(array($ban['ipstart'], $ban['ipend'])); | |||
modLog("Removed ban #{$ban_id} for " . | |||
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$mask\">$mask</a>" : $mask)); | |||
} | |||
query("DELETE FROM ``bans`` WHERE `id` = " . (int)$ban_id) or error(db_error()); | |||
return true; | |||
} | |||
static public function new_ban($mask, $reason, $length = false, $board = false, $mod_id = false) { | |||
global $mod, $pdo; | |||
if ($mod_id === false) { | |||
$mod_id = isset($mod['id']) ? $mod['id'] : -1; | |||
} | |||
$range = self::parse_range($mask); | |||
$mask = self::range_to_string($range); | |||
$query = prepare("INSERT INTO ``bans`` VALUES (NULL, :ipstart, :ipend, :time, :expires, :board, :mod, :reason, 0, NULL)"); | |||
$query->bindValue(':ipstart', $range[0]); | |||
if ($range[1] !== false && $range[1] != $range[0]) | |||
$query->bindValue(':ipend', $range[1]); | |||
else | |||
$query->bindValue(':ipend', null, PDO::PARAM_NULL); | |||
$query->bindValue(':mod', $mod_id); | |||
$query->bindValue(':time', time()); | |||
if ($reason !== '') { | |||
$reason = escape_markup_modifiers($reason); | |||
markup($reason); | |||
$query->bindValue(':reason', $reason); | |||
} else | |||
$query->bindValue(':reason', null, PDO::PARAM_NULL); | |||
if ($length) { | |||
if (is_int($length) || ctype_digit($length)) { | |||
$length = time() + $length; | |||
} else { | |||
$length = self::parse_time($length); | |||
} | |||
$query->bindValue(':expires', $length); | |||
} else { | |||
$query->bindValue(':expires', null, PDO::PARAM_NULL); | |||
} | |||
if ($board) | |||
$query->bindValue(':board', $board); | |||
else | |||
$query->bindValue(':board', null, PDO::PARAM_NULL); | |||
$query->execute() or error(db_error($query)); | |||
if (isset($mod['id']) && $mod['id'] == $mod_id) { | |||
modLog('Created a new ' . | |||
($length > 0 ? preg_replace('/^(\d+) (\w+?)s?$/', '$1-$2', until($length)) : 'permanent') . | |||
' ban on ' . | |||
($board ? '/' . $board . '/' : 'all boards') . | |||
' for ' . | |||
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$mask\">$mask</a>" : $mask) . | |||
' (<small>#' . $pdo->lastInsertId() . '</small>)' . | |||
' with ' . ($reason ? 'reason: ' . utf8tohtml($reason) . '' : 'no reason')); | |||
} | |||
return $pdo->lastInsertId(); | |||
} | |||
} |
@@ -4,10 +4,7 @@ | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { | |||
// You cannot request this file directly. | |||
exit; | |||
} | |||
defined('TINYBOARD') or exit; | |||
class Cache { | |||
private static $cache; | |||
@@ -135,7 +132,7 @@ class Cache { | |||
case 'apc': | |||
return apc_clear_cache('user'); | |||
case 'php': | |||
self::$cache[$key] = array(); | |||
self::$cache = array(); | |||
break; | |||
case 'redis': | |||
if (!self::$cache) | |||
@@ -44,6 +44,8 @@ | |||
$config['debug'] = false; | |||
// For development purposes. Displays (and "dies" on) all errors and warnings. Turn on with the above. | |||
$config['verbose_errors'] = true; | |||
// EXPLAIN all SQL queries (when in debug mode). | |||
$config['debug_explain'] = false; | |||
// Directory where temporary files will be created. | |||
$config['tmp'] = sys_get_temp_dir(); | |||
@@ -168,13 +170,6 @@ | |||
* ==================== | |||
*/ | |||
// Minimum time between between each post by the same IP address. | |||
$config['flood_time'] = 10; | |||
// Minimum time between between each post with the exact same content AND same IP address. | |||
$config['flood_time_ip'] = 120; | |||
// Same as above but by a different IP address. (Same content, not necessarily same IP address.) | |||
$config['flood_time_same'] = 30; | |||
/* | |||
* 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 | |||
@@ -237,6 +232,9 @@ | |||
// How soon after regeneration do hashes expire (in seconds)? | |||
$config['spam']['hidden_inputs_expire'] = 60 * 60 * 3; // three hours | |||
// Whether to use Unicode characters in hidden input names and values. | |||
$config['spam']['unicode'] = true; | |||
// 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( | |||
@@ -274,6 +272,7 @@ | |||
'quick-reply', | |||
'page', | |||
'file_url', | |||
'json_response', | |||
); | |||
// Enable reCaptcha to make spam even harder. Rarely necessary. | |||
@@ -283,6 +282,132 @@ | |||
$config['recaptcha_public'] = '6LcXTcUSAAAAAKBxyFWIt2SO8jwx4W7wcSMRoN3f'; | |||
$config['recaptcha_private'] = '6LcXTcUSAAAAAOGVbVdhmEM1_SyRF4xTKe8jbzf_'; | |||
/* | |||
* 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. | |||
* | |||
* By default, the custom filters array is populated with basic flood prevention conditions. This | |||
* includes forcing users to wait at least 5 seconds between posts. To disable (or amend) these flood | |||
* prevention settings, you will need to empty the $config['filters'] array first. You can do so by | |||
* adding "$config['filters'] = array();" to inc/instance-config.php. Basic flood prevention used to be | |||
* controlled solely by config variables such as $config['flood_time'] and $config['flood_time_ip'], and | |||
* it still is, as long as you leave the relevant $config['filters'] intact. These old config variables | |||
* still exist for backwards-compatability and general convenience. | |||
* | |||
* Read more: http://tinyboard.org/docs/index.php?p=Config/Filters | |||
*/ | |||
// Minimum time between between each post by the same IP address. | |||
$config['flood_time'] = 10; | |||
// Minimum time between between each post with the exact same content AND same IP address. | |||
$config['flood_time_ip'] = 120; | |||
// Same as above but by a different IP address. (Same content, not necessarily same IP address.) | |||
$config['flood_time_same'] = 30; | |||
// Minimum time between posts by the same IP address (all boards). | |||
$config['filters'][] = array( | |||
'condition' => array( | |||
'flood-match' => array('ip'), // Only match IP address | |||
'flood-time' => &$config['flood_time'] | |||
), | |||
'action' => 'reject', | |||
'message' => &$config['error']['flood'] | |||
); | |||
// Minimum time between posts by the same IP address with the same text. | |||
$config['filters'][] = array( | |||
'condition' => array( | |||
'flood-match' => array('ip', 'body'), // Match IP address and post body | |||
'flood-time' => &$config['flood_time_ip'], | |||
'!body' => '/^$/', // Post body is NOT empty | |||
), | |||
'action' => 'reject', | |||
'message' => &$config['error']['flood'] | |||
); | |||
// Minimum time between posts with the same text. (Same content, but not always the same IP address.) | |||
$config['filters'][] = array( | |||
'condition' => array( | |||
'flood-match' => array('body'), // Match only post body | |||
'flood-time' => &$config['flood_time_same'] | |||
), | |||
'action' => 'reject', | |||
'message' => &$config['error']['flood'] | |||
); | |||
// Example: Minimum time between posts with the same file hash. | |||
// $config['filters'][] = array( | |||
// 'condition' => array( | |||
// 'flood-match' => array('file'), // Match file hash | |||
// 'flood-time' => 60 * 2 // 2 minutes minimum | |||
// ), | |||
// 'action' => 'reject', | |||
// 'message' => &$config['error']['flood'] | |||
// ); | |||
// Example: Use the "flood-count" condition to only match if the user has made at least two posts with | |||
// the same content and IP address in the past 2 minutes. | |||
// $config['filters'][] = array( | |||
// 'condition' => array( | |||
// 'flood-match' => array('ip', 'body'), // Match IP address and post body | |||
// 'flood-time' => 60 * 2, // 2 minutes | |||
// 'flood-count' => 2 // At least two recent posts | |||
// ), | |||
// '!body' => '/^$/', | |||
// 'action' => 'reject', | |||
// 'message' => &$config['error']['flood'] | |||
// ); | |||
// Example: 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.' | |||
// ); | |||
// Example: 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.' | |||
// ); | |||
// Example: 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' | |||
// ); | |||
// Filter flood prevention conditions ("flood-match") depend on a table which contains a cache of recent | |||
// posts across all boards. This table is automatically purged of older posts, determining the maximum | |||
// "age" by looking at each filter. However, when determining the maximum age, Tinyboard does not look | |||
// outside the current board. This means that if you have a special flood condition for a specific board | |||
// (contained in a board configuration file) which has a flood-time greater than any of those in the | |||
// global configuration, you need to set the following variable to the maximum flood-time condition value. | |||
// $config['flood_cache'] = 60 * 60 * 24; // 24 hours | |||
/* | |||
* ==================== | |||
* Post settings | |||
@@ -406,57 +531,6 @@ | |||
// 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 | |||
@@ -598,10 +672,6 @@ | |||
// that as a thumbnail instead of resizing/redrawing. | |||
$config['minimum_copy_resize'] = false; | |||
// 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. | |||
@@ -752,6 +822,12 @@ | |||
// Whether or not to put brackets around the whole board list | |||
$config['boardlist_wrap_bracket'] = false; | |||
// Show page navigation links at the top as well. | |||
$config['page_nav_top'] = false; | |||
// Show "Catalog" link in page navigation. Use with the Catalog theme. | |||
// $config['catalog_link'] = 'catalog.html'; | |||
// Board categories. Only used in the "Categories" theme. | |||
// $config['categories'] = array( | |||
// 'Group Name' => array('a', 'b', 'c'), | |||
@@ -1010,8 +1086,8 @@ | |||
* ==================== | |||
*/ | |||
// Limit how many bans can be removed via the ban list. Set to -1 for no limit. | |||
$config['mod']['unban_limit'] = -1; | |||
// Limit how many bans can be removed via the ban list. Set to false (or zero) for no limit. | |||
$config['mod']['unban_limit'] = false; | |||
// Whether or not to lock moderator sessions to IP addresses. This makes cookie theft ineffective. | |||
$config['mod']['lock_ip'] = true; | |||
@@ -1056,14 +1132,6 @@ | |||
// '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"). 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 and ban evaders. Again, a little more database load. | |||
$config['ban_cidr'] = true; | |||
// Enable the moving of single replies | |||
$config['move_replies'] = false; | |||
@@ -1130,18 +1198,28 @@ | |||
* ==================== | |||
*/ | |||
// Probably best not to change these: | |||
if (!defined('JANITOR')) { | |||
define('JANITOR', 0, true); | |||
define('MOD', 1, true); | |||
define('ADMIN', 2, true); | |||
define('DISABLED', 3, true); | |||
} | |||
// Probably best not to change this unless you are smart enough to figure out what you're doing. If you | |||
// decide to change it, remember that it is impossible to redefinite/overwrite groups; you may only add | |||
// new ones. | |||
$config['mod']['groups'] = array( | |||
10 => 'Janitor', | |||
20 => 'Mod', | |||
30 => 'Admin', | |||
// 98 => 'God', | |||
99 => 'Disabled' | |||
); | |||
// If you add stuff to the above, you'll need to call this function immediately after. | |||
define_groups(); | |||
// Example: Adding a new permissions group. | |||
// $config['mod']['groups'][0] = 'NearlyPowerless'; | |||
// define_groups(); | |||
// Capcode permissions. | |||
$config['mod']['capcode'] = array( | |||
// JANITOR => array('Janitor'), | |||
MOD => array('Mod'), | |||
MOD => array('Mod'), | |||
ADMIN => true | |||
); | |||
@@ -1193,7 +1271,8 @@ | |||
// Post bypass unoriginal content check on robot-enabled boards | |||
$config['mod']['postunoriginal'] = ADMIN; | |||
// Bypass flood check | |||
$config['mod']['flood'] = ADMIN; | |||
$config['mod']['bypass_filters'] = ADMIN; | |||
$config['mod']['flood'] = &$config['mod']['bypass_filters']; | |||
// Raw HTML posting | |||
$config['mod']['rawhtml'] = ADMIN; | |||
@@ -1279,18 +1358,14 @@ | |||
$config['mod']['edit_config'] = ADMIN; | |||
// Config editor permissions | |||
$config['mod']['config'] = array( | |||
JANITOR => false, | |||
MOD => false, | |||
ADMIN => false, | |||
DISABLED => false, | |||
); | |||
$config['mod']['config'] = array(); | |||
// Disable the following configuration variables from being changed via ?/config. The following default | |||
// banned variables are considered somewhat dangerous. | |||
$config['mod']['config'][DISABLED] = array( | |||
'mod>config', | |||
'mod>config_editor_php', | |||
'mod>groups', | |||
'convert_args', | |||
'db>password', | |||
); | |||
@@ -1421,6 +1496,3 @@ | |||
// 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}'; | |||
// Regex for URLs. | |||
$config['url_regex'] = '@^(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))$@'; | |||
@@ -4,27 +4,32 @@ | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { | |||
// You cannot request this file directly. | |||
exit; | |||
} | |||
defined('TINYBOARD') or exit; | |||
class PreparedQueryDebug { | |||
protected $query; | |||
protected $query, $explain_query = false; | |||
public function __construct($query) { | |||
global $pdo; | |||
global $pdo, $config; | |||
$query = preg_replace("/[\n\t]+/", ' ', $query); | |||
$this->query = $pdo->prepare($query); | |||
if ($config['debug'] && $config['debug_explain'] && preg_match('/^(SELECT|INSERT|UPDATE|DELETE) /i', $query)) | |||
$this->explain_query = $pdo->prepare("EXPLAIN $query"); | |||
} | |||
public function __call($function, $args) { | |||
global $config, $debug; | |||
if ($config['debug'] && $function == 'execute') { | |||
if ($this->explain_query) { | |||
$this->explain_query->execute() or error(db_error($this->explain_query)); | |||
} | |||
$start = microtime(true); | |||
} | |||
if ($this->explain_query && $function == 'bindValue') | |||
call_user_func_array(array($this->explain_query, $function), $args); | |||
$return = call_user_func_array(array($this->query, $function), $args); | |||
if ($config['debug'] && $function == 'execute') { | |||
@@ -32,6 +37,7 @@ class PreparedQueryDebug { | |||
$debug['sql'][] = array( | |||
'query' => $this->query->queryString, | |||
'rows' => $this->query->rowCount(), | |||
'explain' => $this->explain_query ? $this->explain_query->fetchAll(PDO::FETCH_ASSOC) : null, | |||
'time' => '~' . round($time * 1000, 2) . 'ms' | |||
); | |||
$debug['time']['db_queries'] += $time; | |||
@@ -121,6 +127,9 @@ function query($query) { | |||
sql_open(); | |||
if ($config['debug']) { | |||
if ($config['debug_explain'] && preg_match('/^(SELECT|INSERT|UPDATE|DELETE) /i', $query)) { | |||
$explain = $pdo->query("EXPLAIN $query") or error(db_error()); | |||
} | |||
$start = microtime(true); | |||
$query = $pdo->query($query); | |||
if (!$query) | |||
@@ -129,6 +138,7 @@ function query($query) { | |||
$debug['sql'][] = array( | |||
'query' => $query->queryString, | |||
'rows' => $query->rowCount(), | |||
'explain' => isset($explain) ? $explain->fetchAll(PDO::FETCH_ASSOC) : null, | |||
'time' => '~' . round($time * 1000, 2) . 'ms' | |||
); | |||
$debug['time']['db_queries'] += $time; | |||
@@ -88,6 +88,14 @@ function error($message, $priority = true, $debug_stuff = false) { | |||
// Return the bad request header, necessary for AJAX posts | |||
header($_SERVER['SERVER_PROTOCOL'] . ' 400 Bad Request'); | |||
// Is there a reason to disable this? | |||
if (isset($_POST['json_response'])) { | |||
header('Content-Type: text/json; charset=utf-8'); | |||
die(json_encode(array( | |||
'error' => $message | |||
))); | |||
} | |||
die(Element('page.html', array( | |||
'config' => $config, | |||
'title' => _('Error'), | |||
@@ -536,6 +544,8 @@ class Thread { | |||
$hasnoko50 = $this->postCount() >= $config['noko50_min']; | |||
event('show-thread', $this); | |||
$built = Element('post_thread.html', array('config' => $config, 'board' => $board, 'post' => &$this, 'index' => $index, 'hasnoko50' => $hasnoko50, 'isnoko50' => $isnoko50)); | |||
return $built; | |||
@@ -4,10 +4,7 @@ | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { | |||
// You cannot request this file directly. | |||
exit; | |||
} | |||
defined('TINYBOARD') or exit; | |||
function event() { | |||
global $events; | |||
@@ -4,12 +4,10 @@ | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { | |||
// You cannot request this file directly. | |||
exit; | |||
} | |||
defined('TINYBOARD') or exit; | |||
class Filter { | |||
public $flood_check; | |||
private $condition; | |||
public function __construct(array $arr) { | |||
@@ -25,6 +23,64 @@ class Filter { | |||
if (!is_callable($match)) | |||
error('Custom condition for filter is not callable!'); | |||
return $match($post); | |||
case 'flood-match': | |||
if (!is_array($match)) | |||
error('Filter condition "flood-match" must be an array.'); | |||
// Filter out "flood" table entries which do not match this filter. | |||
$flood_check_matched = array(); | |||
foreach ($this->flood_check as $flood_post) { | |||
foreach ($match as $flood_match_arg) { | |||
switch ($flood_match_arg) { | |||
case 'ip': | |||
if ($flood_post['ip'] != $_SERVER['REMOTE_ADDR']) | |||
continue 3; | |||
break; | |||
case 'body': | |||
if ($flood_post['posthash'] != make_comment_hex($post['body_nomarkup'])) | |||
continue 3; | |||
break; | |||
case 'file': | |||
if (!isset($post['filehash'])) | |||
return false; | |||
if ($flood_post['filehash'] != $post['filehash']) | |||
continue 3; | |||
break; | |||
case 'board': | |||
if ($flood_post['board'] != $post['board']) | |||
continue 3; | |||
break; | |||
case 'isreply': | |||
if ($flood_post['isreply'] == $post['op']) | |||
continue 3; | |||
break; | |||
default: | |||
error('Invalid filter flood condition: ' . $flood_match_arg); | |||
} | |||
} | |||
$flood_check_matched[] = $flood_post; | |||
} | |||
$this->flood_check = $flood_check_matched; | |||
return !empty($this->flood_check); | |||
case 'flood-time': | |||
foreach ($this->flood_check as $flood_post) { | |||
if (time() - $flood_post['time'] <= $match) { | |||
return true; | |||
} | |||
} | |||
return false; | |||
case 'flood-count': | |||
$count = 0; | |||
foreach ($this->flood_check as $flood_post) { | |||
if (time() - $flood_post['time'] <= $this->condition['flood-time']) { | |||
++$count; | |||
} | |||
} | |||
return $count >= $match; | |||
case 'name': | |||
return preg_match($match, $post['name']); | |||
case 'trip': | |||
@@ -34,7 +90,7 @@ class Filter { | |||
case 'subject': | |||
return preg_match($match, $post['subject']); | |||
case 'body': | |||
return preg_match($match, $post['body']); | |||
return preg_match($match, $post['body_nomarkup']); | |||
case 'filename': | |||
if (!$post['has_file']) | |||
return false; | |||
@@ -59,52 +115,18 @@ class Filter { | |||
switch($this->action) { | |||
case 'reject': | |||
error(isset($this->message) ? $this->message : 'Posting throttled by flood filter.'); | |||
error(isset($this->message) ? $this->message : 'Posting throttled by filter.'); | |||
case 'ban': | |||
if (!isset($this->reason)) | |||
error('The ban action requires a reason.'); | |||
$reason = $this->reason; | |||
if (isset($this->expires)) | |||
$expires = time() + $this->expires; | |||
else | |||
$expires = 0; // Ban indefinitely | |||
if (isset($this->reject)) | |||
$reject = $this->reject; | |||
else | |||
$reject = true; | |||
if (isset($this->all_boards)) | |||
$all_boards = $this->all_boards; | |||
else | |||
$all_boards = false; | |||
$query = prepare("INSERT INTO ``bans`` VALUES (NULL, :ip, :mod, :set, :expires, :reason, :board, 0)"); | |||
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); | |||
$query->bindValue(':mod', -1); | |||
$query->bindValue(':set', time()); | |||
if ($expires) | |||
$query->bindValue(':expires', $expires); | |||
else | |||
$query->bindValue(':expires', null, PDO::PARAM_NULL); | |||
if ($reason) | |||
$query->bindValue(':reason', $reason); | |||
else | |||
$query->bindValue(':reason', null, PDO::PARAM_NULL); | |||
if ($all_boards) | |||
$query->bindValue(':board', null, PDO::PARAM_NULL); | |||
else | |||
$query->bindValue(':board', $board['uri']); | |||
$this->expires = isset($this->expires) ? $this->expires : false; | |||
$this->reject = isset($this->reject) ? $this->reject : true; | |||
$this->all_boards = isset($this->all_boards) ? $this->all_boards : false; | |||
$query->execute() or error(db_error($query)); | |||
Bans::new_ban($_SERVER['REMOTE_ADDR'], $this->reason, $this->expires, $this->all_boards ? false : $board['uri'], -1); | |||
if ($reject) { | |||
if ($this->reject) { | |||
if (isset($this->message)) | |||
error($message); | |||
@@ -120,25 +142,77 @@ class Filter { | |||
public function check(array $post) { | |||
foreach ($this->condition as $condition => $value) { | |||
if (!$this->match($post, $condition, $value)) | |||
if ($condition[0] == '!') { | |||
$NOT = true; | |||
$condition = substr($condition, 1); | |||
} else $NOT = false; | |||
if ($this->match($post, $condition, $value) == $NOT) | |||
return false; | |||
} | |||
/* match */ | |||
return true; | |||
} | |||
} | |||
function purge_flood_table() { | |||
global $config; | |||
// Determine how long we need to keep a cache of posts for flood prevention. Unfortunately, it is not | |||
// aware of flood filters in other board configurations. You can solve this problem by settings the | |||
// config variable $config['flood_cache'] (seconds). | |||
if (isset($config['flood_cache'])) { | |||
$max_time = &$config['flood_cache']; | |||
} else { | |||
$max_time = 0; | |||
foreach ($config['filters'] as $filter) { | |||
if (isset($filter['condition']['flood-time'])) | |||
$max_time = max($max_time, $filter['condition']['flood-time']); | |||
} | |||
} | |||
$time = time() - $max_time; | |||
query("DELETE FROM ``flood`` WHERE `time` < $time") or error(db_error()); | |||
} | |||
function do_filters(array $post) { | |||
global $config; | |||
if (!isset($config['filters'])) | |||
if (!isset($config['filters']) || empty($config['filters'])) | |||
return; | |||
foreach ($config['filters'] as $arr) { | |||
$filter = new Filter($arr); | |||
foreach ($config['filters'] as $filter) { | |||
if (isset($filter['condition']['flood-match'])) { | |||
$has_flood = true; | |||
break; | |||
} | |||
} | |||
if (isset($has_flood)) { | |||
if ($post['has_file']) { | |||
$query = prepare("SELECT * FROM ``flood`` WHERE `ip` = :ip OR `posthash` = :posthash OR `filehash` = :filehash"); | |||
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); | |||
$query->bindValue(':posthash', make_comment_hex($post['body_nomarkup'])); | |||
$query->bindValue(':filehash', $post['filehash']); | |||
} else { | |||
$query = prepare("SELECT * FROM ``flood`` WHERE `ip` = :ip OR `posthash` = :posthash"); | |||
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); | |||
$query->bindValue(':posthash', make_comment_hex($post['body_nomarkup'])); | |||
} | |||
$query->execute() or error(db_error($query)); | |||
$flood_check = $query->fetchAll(PDO::FETCH_ASSOC); | |||
} else { | |||
$flood_check = false; | |||
} | |||
foreach ($config['filters'] as $filter_array) { | |||
$filter = new Filter($filter_array); | |||
$filter->flood_check = $flood_check; | |||
if ($filter->check($post)) | |||
$filter->action(); | |||
} | |||
purge_flood_table(); | |||
} | |||
@@ -9,6 +9,8 @@ if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { | |||
exit; | |||
} | |||
define('TINYBOARD', null); | |||
$microtime_start = microtime(true); | |||
require_once 'inc/display.php'; | |||
@@ -16,6 +18,7 @@ require_once 'inc/template.php'; | |||
require_once 'inc/database.php'; | |||
require_once 'inc/events.php'; | |||
require_once 'inc/api.php'; | |||
require_once 'inc/bans.php'; | |||
require_once 'inc/lib/gettext/gettext.inc'; | |||
// the user is not currently logged in as a moderator | |||
@@ -92,7 +95,7 @@ function loadConfig() { | |||
if (!isset($config['referer_match'])) | |||
if (isset($_SERVER['HTTP_HOST'])) { | |||
$config['referer_match'] = '/^' . | |||
(preg_match($config['url_regex'], $config['root']) ? '' : | |||
(preg_match('@^https?://@', $config['root']) ? '' : | |||
'https?:\/\/' . $_SERVER['HTTP_HOST']) . | |||
preg_quote($config['root'], '/') . | |||
'(' . | |||
@@ -266,6 +269,15 @@ function verbose_error_handler($errno, $errstr, $errfile, $errline) { | |||
)); | |||
} | |||
function define_groups() { | |||
global $config; | |||
foreach ($config['mod']['groups'] as $group_value => $group_name) | |||
defined($group_name) or define($group_name, $group_value, true); | |||
ksort($config['mod']['groups']); | |||
} | |||
function create_antibot($board, $thread = null) { | |||
require_once dirname(__FILE__) . '/anti-bot.php'; | |||
@@ -573,30 +585,6 @@ function listBoards() { | |||
return $boards; | |||
} | |||
function checkFlood($post) { | |||
global $board, $config; | |||
$query = prepare(sprintf("SELECT COUNT(*) FROM ``posts_%s`` WHERE | |||
(`ip` = :ip AND `time` >= :floodtime) | |||
OR | |||
(`ip` = :ip AND :body != '' AND `body_nomarkup` = :body AND `time` >= :floodsameiptime) | |||
OR | |||
(:body != '' AND `body_nomarkup` = :body AND `time` >= :floodsametime) LIMIT 1", $board['uri'])); | |||
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); | |||
$query->bindValue(':body', $post['body']); | |||
$query->bindValue(':floodtime', time()-$config['flood_time'], PDO::PARAM_INT); | |||
$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->fetchColumn(); | |||
if (event('check-flood', $post)) | |||
return true; | |||
return $flood; | |||
} | |||
function until($timestamp) { | |||
$difference = $timestamp - time(); | |||
if ($difference < 60) { | |||
@@ -635,9 +623,7 @@ 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)); | |||
Bans::seen($ban['id']); | |||
} | |||
$ban['ip'] = $_SERVER['REMOTE_ADDR']; | |||
@@ -655,7 +641,7 @@ function displayBan($ban) { | |||
)); | |||
} | |||
function checkBan($board = 0) { | |||
function checkBan($board = false) { | |||
global $config; | |||
if (!isset($_SERVER['REMOTE_ADDR'])) { | |||
@@ -665,67 +651,38 @@ function checkBan($board = 0) { | |||
if (event('check-ban', $board)) | |||
return true; | |||
$query = prepare("SELECT `set`, `expires`, `reason`, `board`, `seen`, `id` FROM ``bans`` WHERE (`board` IS NULL OR `board` = :board) AND `ip` = :ip ORDER BY `expires` IS NULL DESC, `expires` DESC LIMIT 1"); | |||
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); | |||
$query->bindValue(':board', $board); | |||
$query->execute() or error(db_error($query)); | |||
if ($query->rowCount() < 1 && $config['ban_range']) { | |||
$query = prepare("SELECT `set`, `expires`, `reason`, `board`, `seen`, `id` FROM ``bans`` WHERE (`board` IS NULL OR `board` = :board) AND :ip LIKE REPLACE(REPLACE(`ip`, '%', '!%'), '*', '%') ESCAPE '!' ORDER BY `expires` IS NULL DESC, `expires` DESC LIMIT 1"); | |||
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); | |||
$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) | |||
AND ( | |||
`ip` REGEXP '^(\[0-9]+\.\[0-9]+\.\[0-9]+\.\[0-9]+\)\/(\[0-9]+)$' | |||
AND | |||
:ip >= INET_ATON(SUBSTRING_INDEX(`ip`, '/', 1)) | |||
AND | |||
:ip < INET_ATON(SUBSTRING_INDEX(`ip`, '/', 1)) + POW(2, 32 - SUBSTRING_INDEX(`ip`, '/', -1)) | |||
) | |||
ORDER BY `expires` IS NULL DESC, `expires` DESC LIMIT 1"); | |||
$query->bindValue(':ip', ip2long($_SERVER['REMOTE_ADDR'])); | |||
$query->bindValue(':board', $board); | |||
$query->execute() or error(db_error($query)); | |||
} | |||
if ($ban = $query->fetch(PDO::FETCH_ASSOC)) { | |||
$bans = Bans::find($_SERVER['REMOTE_ADDR'], $board); | |||
foreach ($bans as &$ban) { | |||
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)); | |||
Bans::delete($ban['id']); | |||
if ($config['require_ban_view'] && !$ban['seen']) { | |||
if (!isset($_POST['json_response'])) { | |||
displayBan($ban); | |||
} else { | |||
header('Content-Type: text/json'); | |||
die(json_encode(array('error' => true, 'banned' => true))); | |||
} | |||
} | |||
} else { | |||
if (!isset($_POST['json_response'])) { | |||
displayBan($ban); | |||
} else { | |||
header('Content-Type: text/json'); | |||
die(json_encode(array('error' => true, 'banned' => true))); | |||
} | |||
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(); | |||
} | |||
// No reason to keep expired bans in the database (except those that haven't been viewed yet) | |||
function purge_bans() { | |||
global $config; | |||
// 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. | |||
if ($config['cache']['enabled'] && $last_time_purged = cache::get('purged_bans_last')) { | |||
if (time() - $last_time_purged < $config['purge_bans'] ) | |||
return; | |||
} | |||
$query = prepare("DELETE FROM ``bans`` WHERE `expires` IS NOT NULL AND `expires` < :time AND `seen` = 1"); | |||
$query->bindValue(':time', time()); | |||
$query->execute() or error(db_error($query)); | |||
Bans::purge(); | |||
if ($config['cache']['enabled']) | |||
cache::set('purged_bans_last', time()); | |||
@@ -781,6 +738,22 @@ function threadExists($id) { | |||
return false; | |||
} | |||
function insertFloodPost(array $post) { | |||
global $board; | |||
$query = prepare("INSERT INTO ``flood`` VALUES (NULL, :ip, :board, :time, :posthash, :filehash, :isreply)"); | |||
$query->bindValue(':ip', $_SERVER['REMOTE_ADDR']); | |||
$query->bindValue(':board', $board['uri']); | |||
$query->bindValue(':time', time()); | |||
$query->bindValue(':posthash', make_comment_hex($post['body_nomarkup'])); | |||
if ($post['has_file']) | |||
$query->bindValue(':filehash', $post['filehash']); | |||
else | |||
$query->bindValue(':filehash', null, PDO::PARAM_NULL); | |||
$query->bindValue(':isreply', !$post['op'], PDO::PARAM_INT); | |||
$query->execute() or error(db_error($query)); | |||
} | |||
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'])); | |||
@@ -789,19 +762,19 @@ function post(array $post) { | |||
if (!empty($post['subject'])) { | |||
$query->bindValue(':subject', $post['subject']); | |||
} else { | |||
$query->bindValue(':subject', NULL, PDO::PARAM_NULL); | |||
$query->bindValue(':subject', null, PDO::PARAM_NULL); | |||
} | |||
if (!empty($post['email'])) { | |||
$query->bindValue(':email', $post['email']); | |||
} else { | |||
$query->bindValue(':email', NULL, PDO::PARAM_NULL); | |||
$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(':trip', null, PDO::PARAM_NULL); | |||
} | |||
$query->bindValue(':name', $post['name']); | |||
@@ -812,27 +785,27 @@ function post(array $post) { | |||
$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); | |||
$query->bindValue(':sticky', true, PDO::PARAM_INT); | |||
} else { | |||
$query->bindValue(':sticky', 0, PDO::PARAM_INT); | |||
$query->bindValue(':sticky', false, PDO::PARAM_INT); | |||
} | |||
if ($post['op'] && $post['mod'] && isset($post['locked']) && $post['locked']) { | |||
$query->bindValue(':locked', 1, PDO::PARAM_INT); | |||
$query->bindValue(':locked', true, PDO::PARAM_INT); | |||
} else { | |||
$query->bindValue(':locked', 0, PDO::PARAM_INT); | |||
$query->bindValue(':locked', false, 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); | |||
$query->bindValue(':capcode', null, PDO::PARAM_NULL); | |||
} | |||
if (!empty($post['embed'])) { | |||
$query->bindValue(':embed', $post['embed']); | |||
} else { | |||
$query->bindValue(':embed', NULL, PDO::PARAM_NULL); | |||
$query->bindValue(':embed', null, PDO::PARAM_NULL); | |||
} | |||
if ($post['op']) { | |||
@@ -1079,10 +1052,16 @@ function index($page, $mod=false) { | |||
while ($th = $query->fetch(PDO::FETCH_ASSOC)) { | |||
$thread = new Thread($th, $mod ? '?/' : $config['root'], $mod); | |||
if ($config['cache']['enabled'] && $cached = cache::get("thread_index_{$board['uri']}_{$th['id']}")) { | |||
$replies = $cached['replies']; | |||
$omitted = $cached['omitted']; | |||
} else { | |||
if ($config['cache']['enabled']) { | |||
$cached = cache::get("thread_index_{$board['uri']}_{$th['id']}"); | |||
if (isset($cached['replies'], $cached['omitted'])) { | |||
$replies = $cached['replies']; | |||
$omitted = $cached['omitted']; | |||
} else { | |||
unset($cached); | |||
} | |||
} | |||
if (!isset($cached)) { | |||
$posts = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE `thread` = :id ORDER BY `id` DESC LIMIT :limit", $board['uri'])); | |||
$posts->bindValue(':id', $th['id']); | |||
$posts->bindValue(':limit', ($th['sticky'] ? $config['threads_preview_sticky'] : $config['threads_preview']), PDO::PARAM_INT); | |||
@@ -1203,6 +1182,26 @@ function getPages($mod=false) { | |||
return $pages; | |||
} | |||
// Stolen with permission from PlainIB (by Frank Usrs) | |||
function make_comment_hex($str) { | |||
// remove cross-board citations | |||
// the numbers don't matter | |||
$str = preg_replace('!>>>/[A-Za-z0-9]+/!', '', $str); | |||
if (function_exists('iconv')) { | |||
// remove diacritics and other noise | |||
// FIXME: this removes cyrillic entirely | |||
$str = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str); | |||
} | |||
$str = strtolower($str); | |||
// strip all non-alphabet characters | |||
$str = preg_replace('/[^a-z]/', '', $str); | |||
return md5($str); | |||
} | |||
function makerobot($body) { | |||
global $config; | |||
$body = strtolower($body); | |||
@@ -4,10 +4,7 @@ | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { | |||
// You cannot request this file directly. | |||
exit; | |||
} | |||
defined('TINYBOARD') or exit; | |||
class Image { | |||
public $src, $format, $image, $size; | |||
@@ -0,0 +1,20 @@ | |||
Copyright (c) 2013 Jason Morriss | |||
Permission is hereby granted, free of charge, to any person obtaining a copy | |||
of this software and associated documentation files (the "Software"), to deal | |||
in the Software without restriction, including without limitation the rights | |||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
copies of the Software, and to permit persons to whom the Software is furnished | |||
to do so, subject to the following conditions: | |||
The above copyright notice and this permission notice shall be included in all | |||
copies or substantial portions of the Software. | |||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
THE SOFTWARE. | |||
@@ -0,0 +1,293 @@ | |||
<?php | |||
/** | |||
* This file is part of the Lifo\IP PHP Library. | |||
* | |||
* (c) Jason Morriss <lifo2013@gmail.com> | |||
* | |||
* For the full copyright and license information, please view the LICENSE | |||
* file that was distributed with this source code. | |||
*/ | |||
namespace Lifo\IP; | |||
/** | |||
* BCMath helper class. | |||
* | |||
* Provides a handful of BCMath routines that are not included in the native | |||
* PHP library. | |||
* | |||
* Note: The Bitwise functions operate on fixed byte boundaries. For example, | |||
* comparing the following numbers uses X number of bits: | |||
* 0xFFFF and 0xFF will result in comparison of 16 bits. | |||
* 0xFFFFFFFF and 0xF will result in comparison of 32 bits. | |||
* etc... | |||
* | |||
*/ | |||
abstract class BC | |||
{ | |||
// Some common (maybe useless) constants | |||
const MAX_INT_32 = '2147483647'; // 7FFFFFFF | |||
const MAX_UINT_32 = '4294967295'; // FFFFFFFF | |||
const MAX_INT_64 = '9223372036854775807'; // 7FFFFFFFFFFFFFFF | |||
const MAX_UINT_64 = '18446744073709551615'; // FFFFFFFFFFFFFFFF | |||
const MAX_INT_96 = '39614081257132168796771975167'; // 7FFFFFFFFFFFFFFFFFFFFFFF | |||
const MAX_UINT_96 = '79228162514264337593543950335'; // FFFFFFFFFFFFFFFFFFFFFFFF | |||
const MAX_INT_128 = '170141183460469231731687303715884105727'; // 7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF | |||
const MAX_UINT_128 = '340282366920938463463374607431768211455'; // FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF | |||
/** | |||
* BC Math function to convert a HEX string into a DECIMAL | |||
*/ | |||
public static function bchexdec($hex) | |||
{ | |||
if (strlen($hex) == 1) { | |||
return hexdec($hex); | |||
} | |||
$remain = substr($hex, 0, -1); | |||
$last = substr($hex, -1); | |||
return bcadd(bcmul(16, self::bchexdec($remain), 0), hexdec($last), 0); | |||
} | |||
/** | |||
* BC Math function to convert a DECIMAL string into a BINARY string | |||
*/ | |||
public static function bcdecbin($dec, $pad = null) | |||
{ | |||
$bin = ''; | |||
while ($dec) { | |||
$m = bcmod($dec, 2); | |||
$dec = bcdiv($dec, 2, 0); | |||
$bin = abs($m) . $bin; | |||
} | |||
return $pad ? sprintf("%0{$pad}s", $bin) : $bin; | |||
} | |||
/** | |||
* BC Math function to convert a BINARY string into a DECIMAL string | |||
*/ | |||
public static function bcbindec($bin) | |||
{ | |||
$dec = '0'; | |||
for ($i=0, $j=strlen($bin); $i<$j; $i++) { | |||
$dec = bcmul($dec, '2', 0); | |||
$dec = bcadd($dec, $bin[$i], 0); | |||
} | |||
return $dec; | |||
} | |||
/** | |||
* BC Math function to convert a BINARY string into a HEX string | |||
*/ | |||
public static function bcbinhex($bin, $pad = 0) | |||
{ | |||
return self::bcdechex(self::bcbindec($bin)); | |||
} | |||
/** | |||
* BC Math function to convert a DECIMAL into a HEX string | |||
*/ | |||
public static function bcdechex($dec) | |||
{ | |||
$last = bcmod($dec, 16); | |||
$remain = bcdiv(bcsub($dec, $last, 0), 16, 0); | |||
return $remain == 0 ? dechex($last) : self::bcdechex($remain) . dechex($last); | |||
} | |||
/** | |||
* Bitwise AND two arbitrarily large numbers together. | |||
*/ | |||
public static function bcand($left, $right) | |||
{ | |||
$len = self::_bitwise($left, $right); | |||
$value = ''; | |||
for ($i=0; $i<$len; $i++) { | |||
$value .= (($left{$i} + 0) & ($right{$i} + 0)) ? '1' : '0'; | |||
} | |||
return self::bcbindec($value != '' ? $value : '0'); | |||
} | |||
/** | |||
* Bitwise OR two arbitrarily large numbers together. | |||
*/ | |||
public static function bcor($left, $right) | |||
{ | |||
$len = self::_bitwise($left, $right); | |||
$value = ''; | |||
for ($i=0; $i<$len; $i++) { | |||
$value .= (($left{$i} + 0) | ($right{$i} + 0)) ? '1' : '0'; | |||
} | |||
return self::bcbindec($value != '' ? $value : '0'); | |||
} | |||
/** | |||
* Bitwise XOR two arbitrarily large numbers together. | |||
*/ | |||
public static function bcxor($left, $right) | |||
{ | |||
$len = self::_bitwise($left, $right); | |||
$value = ''; | |||
for ($i=0; $i<$len; $i++) { | |||
$value .= (($left{$i} + 0) ^ ($right{$i} + 0)) ? '1' : '0'; | |||
} | |||
return self::bcbindec($value != '' ? $value : '0'); | |||
} | |||
/** | |||
* Bitwise NOT two arbitrarily large numbers together. | |||
*/ | |||
public static function bcnot($left, $bits = null) | |||
{ | |||
$right = 0; | |||
$len = self::_bitwise($left, $right, $bits); | |||
$value = ''; | |||
for ($i=0; $i<$len; $i++) { | |||
$value .= $left{$i} == '1' ? '0' : '1'; | |||
} | |||
return self::bcbindec($value); | |||
} | |||
/** | |||
* Shift number to the left | |||
* | |||
* @param integer $bits Total bits to shift | |||
*/ | |||
public static function bcleft($num, $bits) { | |||
return bcmul($num, bcpow('2', $bits)); | |||
} | |||
/** | |||
* Shift number to the right | |||
* | |||
* @param integer $bits Total bits to shift | |||
*/ | |||
public static function bcright($num, $bits) { | |||
return bcdiv($num, bcpow('2', $bits)); | |||
} | |||
/** | |||
* Determine how many bits are needed to store the number rounded to the | |||
* nearest bit boundary. | |||
*/ | |||
public static function bits_needed($num, $boundary = 4) | |||
{ | |||
$bits = 0; | |||
while ($num > 0) { | |||
$num = bcdiv($num, '2', 0); | |||
$bits++; | |||
} | |||
// round to nearest boundrary | |||
return $boundary ? ceil($bits / $boundary) * $boundary : $bits; | |||
} | |||
/** | |||
* BC Math function to return an arbitrarily large random number. | |||
*/ | |||
public static function bcrand($min, $max = null) | |||
{ | |||
if ($max === null) { | |||
$max = $min; | |||
$min = 0; | |||
} | |||
// swap values if $min > $max | |||
if (bccomp($min, $max) == 1) { | |||
list($min,$max) = array($max,$min); | |||
} | |||
return bcadd( | |||
bcmul( | |||
bcdiv( | |||
mt_rand(0, mt_getrandmax()), | |||
mt_getrandmax(), | |||
strlen($max) | |||
), | |||
bcsub( | |||
bcadd($max, '1'), | |||
$min | |||
) | |||
), | |||
$min | |||
); | |||
} | |||
/** | |||
* Computes the natural logarithm using a series. | |||
* @author Thomas Oldbury. | |||
* @license Public domain. | |||
*/ | |||
public static function bclog($num, $iter = 10, $scale = 100) | |||
{ | |||
$log = "0.0"; | |||
for($i = 0; $i < $iter; $i++) { | |||
$pow = 1 + (2 * $i); | |||
$mul = bcdiv("1.0", $pow, $scale); | |||
$fraction = bcmul($mul, bcpow(bcsub($num, "1.0", $scale) / bcadd($num, "1.0", $scale), $pow, $scale), $scale); | |||
$log = bcadd($fraction, $log, $scale); | |||
} | |||
return bcmul("2.0", $log, $scale); | |||
} | |||
/** | |||
* Computes the base2 log using baseN log. | |||
*/ | |||
public static function bclog2($num, $iter = 10, $scale = 100) | |||
{ | |||
return bcdiv(self::bclog($num, $iter, $scale), self::bclog("2", $iter, $scale), $scale); | |||
} | |||
public static function bcfloor($num) | |||
{ | |||
if (substr($num, 0, 1) == '-') { | |||
return bcsub($num, 1, 0); | |||
} | |||
return bcadd($num, 0, 0); | |||
} | |||
public static function bcceil($num) | |||
{ | |||
if (substr($num, 0, 1) == '-') { | |||
return bcsub($num, 0, 0); | |||
} | |||
return bcadd($num, 1, 0); | |||
} | |||
/** | |||
* Compare two numbers and return -1, 0, 1 depending if the LEFT number is | |||
* < = > the RIGHT. | |||
* | |||
* @param string|integer $left Left side operand | |||
* @param string|integer $right Right side operand | |||
* @return integer Return -1,0,1 for <=> comparison | |||
*/ | |||
public static function cmp($left, $right) | |||
{ | |||
// @todo could an optimization be done to determine if a normal 32bit | |||
// comparison could be done instead of using bccomp? But would | |||
// the number verification cause too much overhead to be useful? | |||
return bccomp($left, $right, 0); | |||
} | |||
/** | |||
* Internal function to prepare for bitwise operations | |||
*/ | |||
private static function _bitwise(&$left, &$right, $bits = null) | |||
{ | |||
if ($bits === null) { | |||
$bits = max(self::bits_needed($left), self::bits_needed($right)); | |||
} | |||
$left = self::bcdecbin($left); | |||
$right = self::bcdecbin($right); | |||
$len = max(strlen($left), strlen($right), (int)$bits); | |||
$left = sprintf("%0{$len}s", $left); | |||
$right = sprintf("%0{$len}s", $right); | |||
return $len; | |||
} | |||
} |
@@ -0,0 +1,706 @@ | |||
<?php | |||
/** | |||
* This file is part of the Lifo\IP PHP Library. | |||
* | |||
* (c) Jason Morriss <lifo2013@gmail.com> | |||
* | |||
* For the full copyright and license information, please view the LICENSE | |||
* file that was distributed with this source code. | |||
*/ | |||
namespace Lifo\IP; | |||
/** | |||
* CIDR Block helper class. | |||
* | |||
* Most routines can be used statically or by instantiating an object and | |||
* calling its methods. | |||
* | |||
* Provides routines to do various calculations on IP addresses and ranges. | |||
* Convert to/from CIDR to ranges, etc. | |||
*/ | |||
class CIDR | |||
{ | |||
const INTERSECT_NO = 0; | |||
const INTERSECT_YES = 1; | |||
const INTERSECT_LOW = 2; | |||
const INTERSECT_HIGH = 3; | |||
protected $start; | |||
protected $end; | |||
protected $prefix; | |||
protected $version; | |||
protected $istart; | |||
protected $iend; | |||
private $cache; | |||
/** | |||
* Create a new CIDR object. | |||
* | |||
* The IP range can be arbitrary and does not have to fall on a valid CIDR | |||
* range. Some methods will return different values depending if you ignore | |||
* the prefix or not. By default all prefix sensitive methods will assume | |||
* the prefix is used. | |||
* | |||
* @param string $cidr An IP address (1.2.3.4), CIDR block (1.2.3.4/24), | |||
* or range "1.2.3.4-1.2.3.10" | |||
* @param string $end Ending IP in range if no cidr/prefix is given | |||
*/ | |||
public function __construct($cidr, $end = null) | |||
{ | |||
if ($end !== null) { | |||
$this->setRange($cidr, $end); | |||
} else { | |||
$this->setCidr($cidr); | |||
} | |||
} | |||
/** | |||
* Returns the string representation of the CIDR block. | |||
*/ | |||
public function __toString() | |||
{ | |||
// do not include the prefix if its a single IP | |||
try { | |||
if ($this->isTrueCidr() && ( | |||
($this->version == 4 and $this->prefix != 32) || | |||
($this->version == 6 and $this->prefix != 128) | |||
) | |||
) { | |||
return $this->start . '/' . $this->prefix; | |||
} | |||
} catch (\Exception $e) { | |||
// isTrueCidr() calls getRange which can throw an exception | |||
} | |||
if (strcmp($this->start, $this->end) == 0) { | |||
return $this->start; | |||
} | |||
return $this->start . ' - ' . $this->end; | |||
} | |||
public function __clone() | |||
{ | |||
// do not clone the cache. No real reason why. I just want to keep the | |||
// memory foot print as low as possible, even though this is trivial. | |||
$this->cache = array(); | |||
} | |||
/** | |||
* Set an arbitrary IP range. | |||
* The closest matching prefix will be calculated but the actual range | |||
* stored in the object can be arbitrary. | |||
* @param string $start Starting IP or combination "start-end" string. | |||
* @param string $end Ending IP or null. | |||
*/ | |||
public function setRange($ip, $end = null) | |||
{ | |||
if (strpos($ip, '-') !== false) { | |||
list($ip, $end) = array_map('trim', explode('-', $ip, 2)); | |||
} | |||
if (false === filter_var($ip, FILTER_VALIDATE_IP) || | |||
false === filter_var($end, FILTER_VALIDATE_IP)) { | |||
throw new \InvalidArgumentException("Invalid IP range \"$ip-$end\""); | |||
} | |||
// determine version (4 or 6) | |||
$this->version = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4; | |||
$this->istart = IP::inet_ptod($ip); | |||
$this->iend = IP::inet_ptod($end); | |||
// fix order | |||
if (bccomp($this->istart, $this->iend) == 1) { | |||
list($this->istart, $this->iend) = array($this->iend, $this->istart); | |||
list($ip, $end) = array($end, $ip); | |||
} | |||
$this->start = $ip; | |||
$this->end = $end; | |||
// calculate real prefix | |||
$len = $this->version == 4 ? 32 : 128; | |||
$this->prefix = $len - strlen(BC::bcdecbin(BC::bcxor($this->istart, $this->iend))); | |||
} | |||
/** | |||
* Returns true if the current IP is a true cidr block | |||
*/ | |||
public function isTrueCidr() | |||
{ | |||
return $this->start == $this->getNetwork() && $this->end == $this->getBroadcast(); | |||
} | |||
/** | |||
* Set the CIDR block. | |||
* | |||
* The prefix length is optional and will default to 32 ot 128 depending on | |||
* the version detected. | |||
* | |||
* @param string $cidr CIDR block string, eg: "192.168.0.0/24" or "2001::1/64" | |||
* @throws \InvalidArgumentException If the CIDR block is invalid | |||
*/ | |||
public function setCidr($cidr) | |||
{ | |||
if (strpos($cidr, '-') !== false) { | |||
return $this->setRange($cidr); | |||
} | |||
list($ip, $bits) = array_pad(array_map('trim', explode('/', $cidr, 2)), 2, null); | |||
if (false === filter_var($ip, FILTER_VALIDATE_IP)) { | |||
throw new \InvalidArgumentException("Invalid IP address \"$cidr\""); | |||
} | |||
// determine version (4 or 6) | |||
$this->version = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4; | |||
$this->start = $ip; | |||
$this->istart = IP::inet_ptod($ip); | |||
if ($bits !== null and $bits !== '') { | |||
$this->prefix = $bits; | |||
} else { | |||
$this->prefix = $this->version == 4 ? 32 : 128; | |||
} | |||
if (($this->prefix < 0) | |||
|| ($this->prefix > 32 and $this->version == 4) | |||
|| ($this->prefix > 128 and $this->version == 6)) { | |||
throw new \InvalidArgumentException("Invalid IP address \"$cidr\""); | |||
} | |||
$this->end = $this->getBroadcast(); | |||
$this->iend = IP::inet_ptod($this->end); | |||
$this->cache = array(); | |||
} | |||
/** | |||
* Get the IP version. 4 or 6. | |||
* | |||
* @return integer | |||
*/ | |||
public function getVersion() | |||
{ | |||
return $this->version; | |||
} | |||
/** | |||
* Get the prefix. | |||
* | |||
* Always returns the "proper" prefix, even if the IP range is arbitrary. | |||
* | |||
* @return integer | |||
*/ | |||
public function getPrefix() | |||
{ | |||
return $this->prefix; | |||
} | |||
/** | |||
* Return the starting presentational IP or Decimal value. | |||
* | |||
* Ignores prefix | |||
*/ | |||
public function getStart($decimal = false) | |||
{ | |||
return $decimal ? $this->istart : $this->start; | |||
} | |||
/** | |||
* Return the ending presentational IP or Decimal value. | |||
* | |||
* Ignores prefix | |||
*/ | |||
public function getEnd($decimal = false) | |||
{ | |||
return $decimal ? $this->iend : $this->end; | |||
} | |||
/** | |||
* Return the next presentational IP or Decimal value (following the | |||
* broadcast address of the current CIDR block). | |||
*/ | |||
public function getNext($decimal = false) | |||
{ | |||
$next = bcadd($this->getEnd(true), '1'); | |||
return $decimal ? $next : new self(IP::inet_dtop($next)); | |||
} | |||
/** | |||
* Returns true if the IP is an IPv4 | |||
* | |||
* @return boolean | |||
*/ | |||
public function isIPv4() | |||
{ | |||
return $this->version == 4; | |||
} | |||
/** | |||
* Returns true if the IP is an IPv6 | |||
* | |||
* @return boolean | |||
*/ | |||
public function isIPv6() | |||
{ | |||
return $this->version == 6; | |||
} | |||
/** | |||
* Get the cidr notation for the subnet block. | |||
* | |||
* This is useful for when you want a string representation of the IP/prefix | |||
* and the starting IP is not on a valid network boundrary (eg: Displaying | |||
* an IP from an interface). | |||
* | |||
* @return string IP in CIDR notation "ipaddr/prefix" | |||
*/ | |||
public function getCidr() | |||
{ | |||
return $this->start . '/' . $this->prefix; | |||
} | |||
/** | |||
* Get the [low,high] range of the CIDR block | |||
* | |||
* Prefix sensitive. | |||
* | |||
* @param boolean $ignorePrefix If true the arbitrary start-end range is | |||
* returned. default=false. | |||
*/ | |||
public function getRange($ignorePrefix = false) | |||
{ | |||
$range = $ignorePrefix | |||
? array($this->start, $this->end) | |||
: self::cidr_to_range($this->start, $this->prefix); | |||
// watch out for IP '0' being converted to IPv6 '::' | |||
if ($range[0] == '::' and strpos($range[1], ':') == false) { | |||
$range[0] = '0.0.0.0'; | |||
} | |||
return $range; | |||
} | |||
/** | |||
* Return the IP in its fully expanded form. | |||
* | |||
* For example: 2001::1 == 2007:0000:0000:0000:0000:0000:0000:0001 | |||
* | |||
* @see IP::inet_expand | |||
*/ | |||
public function getExpanded() | |||
{ | |||
return IP::inet_expand($this->start); | |||
} | |||
/** | |||
* Get network IP of the CIDR block | |||
* | |||
* Prefix sensitive. | |||
* | |||
* @param boolean $ignorePrefix If true the arbitrary start-end range is | |||
* returned. default=false. | |||
*/ | |||
public function getNetwork($ignorePrefix = false) | |||
{ | |||
// micro-optimization to prevent calling getRange repeatedly | |||
$k = $ignorePrefix ? 1 : 0; | |||
if (!isset($this->cache['range'][$k])) { | |||
$this->cache['range'][$k] = $this->getRange($ignorePrefix); | |||
} | |||
return $this->cache['range'][$k][0]; | |||
} | |||
/** | |||
* Get broadcast IP of the CIDR block | |||
* | |||
* Prefix sensitive. | |||
* | |||
* @param boolean $ignorePrefix If true the arbitrary start-end range is | |||
* returned. default=false. | |||
*/ | |||
public function getBroadcast($ignorePrefix = false) | |||
{ | |||
// micro-optimization to prevent calling getRange repeatedly | |||
$k = $ignorePrefix ? 1 : 0; | |||
if (!isset($this->cache['range'][$k])) { | |||
$this->cache['range'][$k] = $this->getRange($ignorePrefix); | |||
} | |||
return $this->cache['range'][$k][1]; | |||
} | |||
/** | |||
* Get the network mask based on the prefix. | |||
* | |||
*/ | |||
public function getMask() | |||
{ | |||
return self::prefix_to_mask($this->prefix, $this->version); | |||
} | |||
/** | |||
* Get total hosts within CIDR range | |||
* | |||
* Prefix sensitive. | |||
* | |||
* @param boolean $ignorePrefix If true the arbitrary start-end range is | |||
* returned. default=false. | |||
*/ | |||
public function getTotal($ignorePrefix = false) | |||
{ | |||
// micro-optimization to prevent calling getRange repeatedly | |||
$k = $ignorePrefix ? 1 : 0; | |||
if (!isset($this->cache['range'][$k])) { | |||
$this->cache['range'][$k] = $this->getRange($ignorePrefix); | |||
} | |||
return bcadd(bcsub(IP::inet_ptod($this->cache['range'][$k][1]), | |||
IP::inet_ptod($this->cache['range'][$k][0])), '1'); | |||
} | |||
public function intersects($cidr) | |||
{ | |||
return self::cidr_intersect((string)$this, $cidr); | |||
} | |||
/** | |||
* Determines the intersection between an IP (with optional prefix) and a | |||
* CIDR block. | |||
* | |||
* The IP will be checked against the CIDR block given and will either be | |||
* inside or outside the CIDR completely, or partially. | |||
* | |||
* NOTE: The caller should explicitly check against the INTERSECT_* | |||
* constants because this method will return a value > 1 even for partial | |||
* matches. | |||
* | |||
* @param mixed $ip The IP/cidr to match | |||
* @param mixed $cidr The CIDR block to match within | |||
* @return integer Returns an INTERSECT_* constant | |||
* @throws \InvalidArgumentException if either $ip or $cidr is invalid | |||
*/ | |||
public static function cidr_intersect($ip, $cidr) | |||
{ | |||
// use fixed length HEX strings so we can easily do STRING comparisons | |||
// instead of using slower bccomp() math. | |||
list($lo,$hi) = array_map(function($v){ return sprintf("%032s", IP::inet_ptoh($v)); }, CIDR::cidr_to_range($ip)); | |||
list($min,$max) = array_map(function($v){ return sprintf("%032s", IP::inet_ptoh($v)); }, CIDR::cidr_to_range($cidr)); | |||
/** visualization of logic used below | |||
lo-hi = $ip to check | |||
min-max = $cidr block being checked against | |||
--- --- --- lo --- --- hi --- --- --- --- --- IP/prefix to check | |||
--- min --- --- max --- --- --- --- --- --- --- Partial "LOW" match | |||
--- --- --- --- --- min --- --- max --- --- --- Partial "HIGH" match | |||
--- --- --- --- min max --- --- --- --- --- --- No match "NO" | |||
--- --- --- --- --- --- --- --- min --- max --- No match "NO" | |||
min --- max --- --- --- --- --- --- --- --- --- No match "NO" | |||
--- --- min --- --- --- --- max --- --- --- --- Full match "YES" | |||
*/ | |||
// IP is exact match or completely inside the CIDR block | |||
if ($lo >= $min and $hi <= $max) { | |||
return self::INTERSECT_YES; | |||
} | |||
// IP is completely outside the CIDR block | |||
if ($max < $lo or $min > $hi) { | |||
return self::INTERSECT_NO; | |||
} | |||
// @todo is it useful to return LOW/HIGH partial matches? | |||
// IP matches the lower end | |||
if ($max <= $hi and $min <= $lo) { | |||
return self::INTERSECT_LOW; | |||
} | |||
// IP matches the higher end | |||
if ($min >= $lo and $max >= $hi) { | |||
return self::INTERSECT_HIGH; | |||
} | |||
return self::INTERSECT_NO; | |||
} | |||
/** | |||
* Converts an IPv4 or IPv6 CIDR block into its range. | |||
* | |||
* @todo May not be the fastest way to do this. | |||
* | |||
* @static | |||
* @param string $cidr CIDR block or IP address string. | |||
* @param integer|null $bits If /bits is not specified on string they can be | |||
* passed via this parameter instead. | |||
* @return array A 2 element array with the low, high range | |||
*/ | |||
public static function cidr_to_range($cidr, $bits = null) | |||
{ | |||
if (strpos($cidr, '/') !== false) { | |||
list($ip, $_bits) = array_pad(explode('/', $cidr, 2), 2, null); | |||
} else { | |||
$ip = $cidr; | |||
$_bits = $bits; | |||
} | |||
if (false === filter_var($ip, FILTER_VALIDATE_IP)) { | |||
throw new \InvalidArgumentException("IP address \"$cidr\" is invalid"); | |||
} | |||
// force bit length to 32 or 128 depending on type of IP | |||
$bitlen = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 128 : 32; | |||
if ($bits === null) { | |||
// if no prefix is given use the length of the binary string which | |||
// will give us 32 or 128 and result in a single IP being returned. | |||
$bits = $_bits !== null ? $_bits : $bitlen; | |||
} | |||
if ($bits > $bitlen) { | |||
throw new \InvalidArgumentException("IP address \"$cidr\" is invalid"); | |||
} | |||
$ipdec = IP::inet_ptod($ip); | |||
$ipbin = BC::bcdecbin($ipdec, $bitlen); | |||
// calculate network | |||
$netmask = BC::bcbindec(str_pad(str_repeat('1',$bits), $bitlen, '0')); | |||
$ip1 = BC::bcand($ipdec, $netmask); | |||
// calculate "broadcast" (not technically a broadcast in IPv6) | |||
$ip2 = BC::bcor($ip1, BC::bcnot($netmask)); | |||
return array(IP::inet_dtop($ip1), IP::inet_dtop($ip2)); | |||
} | |||
/** | |||
* Return the CIDR string from the range given | |||
*/ | |||
public static function range_to_cidr($start, $end) | |||
{ | |||
$cidr = new CIDR($start, $end); | |||
return (string)$cidr; | |||
} | |||
/** | |||
* Return the maximum prefix length that would fit the IP address given. | |||
* | |||
* This is useful to determine how my bit would be needed to store the IP | |||
* address when you don't already have a prefix for the IP. | |||
* | |||
* @example 216.240.32.0 would return 27 | |||
* | |||
* @param string $ip IP address without prefix | |||
* @param integer $bits Maximum bits to check; defaults to 32 for IPv4 and 128 for IPv6 | |||
*/ | |||
public static function max_prefix($ip, $bits = null) | |||
{ | |||
static $mask = array(); | |||
$ver = (false === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4; | |||
$max = $ver == 6 ? 128 : 32; | |||
if ($bits === null) { | |||
$bits = $max; | |||
} | |||
$int = IP::inet_ptod($ip); | |||
while ($bits > 0) { | |||
// micro-optimization; calculate mask once ... | |||
if (!isset($mask[$ver][$bits-1])) { | |||
// 2^$max - 2^($max - $bits); | |||
if ($ver == 4) { | |||
$mask[$ver][$bits-1] = pow(2, $max) - pow(2, $max - ($bits-1)); | |||
} else { | |||
$mask[$ver][$bits-1] = bcsub(bcpow(2, $max), bcpow(2, $max - ($bits-1))); | |||
} | |||
} | |||
$m = $mask[$ver][$bits-1]; | |||
//printf("%s/%d: %s & %s == %s\n", $ip, $bits-1, BC::bcdecbin($m, 32), BC::bcdecbin($int, 32), BC::bcdecbin(BC::bcand($int, $m))); | |||
//echo "$ip/", $bits-1, ": ", IP::inet_dtop($m), " ($m) & $int == ", BC::bcand($int, $m), "\n"; | |||
if (bccomp(BC::bcand($int, $m), $int) != 0) { | |||
return $bits; | |||
} | |||
$bits--; | |||
} | |||
return $bits; | |||
} | |||
/** | |||
* Return a contiguous list of true CIDR blocks that span the range given. | |||
* | |||
* Note: It's not a good idea to call this with IPv6 addresses. While it may | |||
* work for certain ranges this can be very slow. Also an IPv6 list won't be | |||
* as accurate as an IPv4 list. | |||
* | |||
* @example | |||
* range_to_cidrlist(192.168.0.0, 192.168.0.15) == | |||
* 192.168.0.0/28 | |||
* range_to_cidrlist(192.168.0.0, 192.168.0.20) == | |||
* 192.168.0.0/28 | |||
* 192.168.0.16/30 | |||
* 192.168.0.20/32 | |||
*/ | |||
public static function range_to_cidrlist($start, $end) | |||
{ | |||
$ver = (false === filter_var($start, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) ? 6 : 4; | |||
$start = IP::inet_ptod($start); | |||
$end = IP::inet_ptod($end); | |||
$len = $ver == 4 ? 32 : 128; | |||
$log2 = $ver == 4 ? log(2) : BC::bclog(2); | |||
$list = array(); | |||
while (BC::cmp($end, $start) >= 0) { // $end >= $start | |||
$prefix = self::max_prefix(IP::inet_dtop($start), $len); | |||
if ($ver == 4) { | |||
$diff = $len - floor( log($end - $start + 1) / $log2 ); | |||
} else { | |||
// this is not as accurate due to the bclog function | |||
$diff = bcsub($len, BC::bcfloor(bcdiv(BC::bclog(bcadd(bcsub($end, $start), '1')), $log2))); | |||
} | |||
if ($prefix < $diff) { | |||
$prefix = $diff; | |||
} | |||
$list[] = IP::inet_dtop($start) . "/" . $prefix; | |||
if ($ver == 4) { | |||
$start += pow(2, $len - $prefix); | |||
} else { | |||
$start = bcadd($start, bcpow(2, $len - $prefix)); | |||
} | |||
} | |||
return $list; | |||
} | |||
/** | |||
* Return an list of optimized CIDR blocks by collapsing adjacent CIDR | |||
* blocks into larger blocks. | |||
* | |||
* @param array $cidrs List of CIDR block strings or objects | |||
* @param integer $maxPrefix Maximum prefix to allow | |||
* @return array Optimized list of CIDR objects | |||
*/ | |||
public static function optimize_cidrlist($cidrs, $maxPrefix = 32) | |||
{ | |||
// all indexes must be a CIDR object | |||
$cidrs = array_map(function($o){ return $o instanceof CIDR ? $o : new CIDR($o); }, $cidrs); | |||
// sort CIDR blocks in proper order so we can easily loop over them | |||
$cidrs = self::cidr_sort($cidrs); | |||
$list = array(); | |||
while ($cidrs) { | |||
$c = array_shift($cidrs); | |||
$start = $c->getStart(); | |||
$max = bcadd($c->getStart(true), $c->getTotal()); | |||
// loop through each cidr block until its ending range is more than | |||
// the current maximum. | |||
while (!empty($cidrs) and $cidrs[0]->getStart(true) <= $max) { | |||
$b = array_shift($cidrs); | |||
$newmax = bcadd($b->getStart(true), $b->getTotal()); | |||
if ($newmax > $max) { | |||
$max = $newmax; | |||
} | |||
} | |||
// add the new cidr range to the optimized list | |||
$list = array_merge($list, self::range_to_cidrlist($start, IP::inet_dtop(bcsub($max, '1')))); | |||
} | |||
return $list; | |||
} | |||
/** | |||
* Sort the list of CIDR blocks, optionally with a custom callback function. | |||
* | |||
* @param array $cidrs A list of CIDR blocks (strings or objects) | |||
* @param Closure $callback Optional callback to perform the sorting. | |||
* See PHP usort documentation for more details. | |||
*/ | |||
public static function cidr_sort($cidrs, $callback = null) | |||
{ | |||
// all indexes must be a CIDR object | |||
$cidrs = array_map(function($o){ return $o instanceof CIDR ? $o : new CIDR($o); }, $cidrs); | |||
if ($callback === null) { | |||
$callback = function($a, $b) { | |||
if (0 != ($o = BC::cmp($a->getStart(true), $b->getStart(true)))) { | |||
return $o; // < or > | |||
} | |||
if ($a->getPrefix() == $b->getPrefix()) { | |||
return 0; | |||
} | |||
return $a->getPrefix() < $b->getPrefix() ? -1 : 1; | |||
}; | |||
} elseif (!($callback instanceof \Closure) or !is_callable($callback)) { | |||
throw new \InvalidArgumentException("Invalid callback in CIDR::cidr_sort, expected Closure, got " . gettype($callback)); | |||
} | |||
usort($cidrs, $callback); | |||
return $cidrs; | |||
} | |||
/** | |||
* Return the Prefix bits from the IPv4 mask given. | |||
* | |||
* This is only valid for IPv4 addresses since IPv6 addressing does not | |||
* have a concept of network masks. | |||
* | |||
* Example: 255.255.255.0 == 24 | |||
* | |||
* @param string $mask IPv4 network mask. | |||
*/ | |||
public static function mask_to_prefix($mask) | |||
{ | |||
if (false === filter_var($mask, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { | |||
throw new \InvalidArgumentException("Invalid IP netmask \"$mask\""); | |||
} | |||
return strrpos(IP::inet_ptob($mask, 32), '1') + 1; | |||
} | |||
/** | |||
* Return the network mask for the prefix given. | |||
* | |||
* Normally this is only useful for IPv4 addresses but you can generate a | |||
* mask for IPv6 addresses as well, only because its mathematically | |||
* possible. | |||
* | |||
* @param integer $prefix CIDR prefix bits (0-128) | |||
* @param integer $version IP version. If null the version will be detected | |||
* based on the prefix length given. | |||
*/ | |||
public static function prefix_to_mask($prefix, $version = null) | |||
{ | |||
if ($version === null) { | |||
$version = $prefix > 32 ? 6 : 4; | |||
} | |||
if ($prefix < 0 or $prefix > 128) { | |||
throw new \InvalidArgumentException("Invalid prefix length \"$prefix\""); | |||
} | |||
if ($version != 4 and $version != 6) { | |||
throw new \InvalidArgumentException("Invalid version \"$version\". Must be 4 or 6"); | |||
} | |||
if ($version == 4) { | |||
return long2ip($prefix == 0 ? 0 : (0xFFFFFFFF >> (32 - $prefix)) << (32 - $prefix)); | |||
} else { | |||
return IP::inet_dtop($prefix == 0 ? 0 : BC::bcleft(BC::bcright(BC::MAX_UINT_128, 128-$prefix), 128-$prefix)); | |||
} | |||
} | |||
/** | |||
* Return true if the $ip given is a true CIDR block. | |||
* | |||
* A true CIDR block is one where the $ip given is the actual Network | |||
* address and broadcast matches the prefix appropriately. | |||
*/ | |||
public static function cidr_is_true($ip) | |||
{ | |||
$ip = new CIDR($ip); | |||
return $ip->isTrueCidr(); | |||
} | |||
} |
@@ -0,0 +1,207 @@ | |||
<?php | |||
/** | |||
* This file is part of the Lifo\IP PHP Library. | |||
* | |||
* (c) Jason Morriss <lifo2013@gmail.com> | |||
* | |||
* For the full copyright and license information, please view the LICENSE | |||
* file that was distributed with this source code. | |||
*/ | |||
namespace Lifo\IP; | |||
/** | |||
* IP Address helper class. | |||
* | |||
* Provides routines to translate IPv4 and IPv6 addresses between human readable | |||
* strings, decimal, hexidecimal and binary. | |||
* | |||
* Requires BCmath extension and IPv6 PHP support | |||
*/ | |||
abstract class IP | |||
{ | |||
/** | |||
* Convert a human readable (presentational) IP address string into a decimal string. | |||
*/ | |||
public static function inet_ptod($ip) | |||
{ | |||
// shortcut for IPv4 addresses | |||
if (strpos($ip, ':') === false && strpos($ip, '.') !== false) { | |||
return sprintf('%u', ip2long($ip)); | |||
} | |||
// remove any cidr block notation | |||
if (($o = strpos($ip, '/')) !== false) { | |||
$ip = substr($ip, 0, $o); | |||
} | |||
// unpack into 4 32bit integers | |||
$parts = unpack('N*', inet_pton($ip)); | |||
foreach ($parts as &$part) { | |||
if ($part < 0) { | |||
// convert signed int into unsigned | |||
$part = sprintf('%u', $part); | |||
//$part = bcadd($part, '4294967296'); | |||
} | |||
} | |||
// add each 32bit integer to the proper bit location in our big decimal | |||
$decimal = $parts[4]; // << 0 | |||
$decimal = bcadd($decimal, bcmul($parts[3], '4294967296')); // << 32 | |||
$decimal = bcadd($decimal, bcmul($parts[2], '18446744073709551616')); // << 64 | |||
$decimal = bcadd($decimal, bcmul($parts[1], '79228162514264337593543950336')); // << 96 | |||
return $decimal; | |||
} | |||
/** | |||
* Convert a decimal string into a human readable IP address. | |||
*/ | |||
public static function inet_dtop($decimal, $expand = false) | |||
{ | |||
$parts = array(); | |||
$parts[1] = bcdiv($decimal, '79228162514264337593543950336', 0); // >> 96 | |||
$decimal = bcsub($decimal, bcmul($parts[1], '79228162514264337593543950336')); | |||
$parts[2] = bcdiv($decimal, '18446744073709551616', 0); // >> 64 | |||
$decimal = bcsub($decimal, bcmul($parts[2], '18446744073709551616')); | |||
$parts[3] = bcdiv($decimal, '4294967296', 0); // >> 32 | |||
$decimal = bcsub($decimal, bcmul($parts[3], '4294967296')); | |||
$parts[4] = $decimal; // >> 0 | |||
foreach ($parts as &$part) { | |||
if (bccomp($part, '2147483647') == 1) { | |||
$part = bcsub($part, '4294967296'); | |||
} | |||
$part = (int) $part; | |||
} | |||
// if the first 96bits is all zeros then we can safely assume we | |||
// actually have an IPv4 address. Even though it's technically possible | |||
// you're not really ever going to see an IPv6 address in the range: | |||
// ::0 - ::ffff | |||
// It's feasible to see an IPv6 address of "::", in which case the | |||
// caller is going to have to account for that on their own. | |||
if (($parts[1] | $parts[2] | $parts[3]) == 0) { | |||
$ip = long2ip($parts[4]); | |||
} else { | |||
$packed = pack('N4', $parts[1], $parts[2], $parts[3], $parts[4]); | |||
$ip = inet_ntop($packed); | |||
} | |||
// Turn IPv6 to IPv4 if it's IPv4 | |||
if (preg_match('/^::\d+\./', $ip)) { | |||
return substr($ip, 2); | |||
} | |||
return $expand ? self::inet_expand($ip) : $ip; | |||
} | |||
/** | |||
* Convert a human readable (presentational) IP address into a HEX string. | |||
*/ | |||
public static function inet_ptoh($ip) | |||
{ | |||
return bin2hex(inet_pton($ip)); | |||
//return BC::bcdechex(self::inet_ptod($ip)); | |||
} | |||
/** | |||
* Convert a human readable (presentational) IP address into a BINARY string. | |||
*/ | |||
public static function inet_ptob($ip, $bits = 128) | |||
{ | |||
return BC::bcdecbin(self::inet_ptod($ip), $bits); | |||
} | |||
/** | |||
* Convert a binary string into an IP address (presentational) string. | |||
*/ | |||
public static function inet_btop($bin) | |||
{ | |||
return self::inet_dtop(BC::bcbindec($bin)); | |||
} | |||
/** | |||
* Convert a HEX string into a human readable (presentational) IP address | |||
*/ | |||
public static function inet_htop($hex) | |||
{ | |||
return self::inet_dtop(BC::bchexdec($hex)); | |||
} | |||
/** | |||
* Expand an IP address. IPv4 addresses are returned as-is. | |||
* | |||
* Example: | |||
* 2001::1 expands to 2001:0000:0000:0000:0000:0000:0000:0001 | |||
* ::127.0.0.1 expands to 0000:0000:0000:0000:0000:0000:7f00:0001 | |||
* 127.0.0.1 expands to 127.0.0.1 | |||
*/ | |||
public static function inet_expand($ip) | |||
{ | |||
// strip possible cidr notation off | |||
if (($pos = strpos($ip, '/')) !== false) { | |||
$ip = substr($ip, 0, $pos); | |||
} | |||
$bytes = unpack('n*', inet_pton($ip)); | |||
if (count($bytes) > 2) { | |||
return implode(':', array_map(function ($b) { | |||
return sprintf("%04x", $b); | |||
}, $bytes)); | |||
} | |||
return $ip; | |||
} | |||
/** | |||
* Convert an IPv4 address into an IPv6 address. | |||
* | |||
* One use-case for this is IP 6to4 tunnels used in networking. | |||
* | |||
* @example | |||
* to_ipv4("10.10.10.10") == a0a:a0a | |||
* | |||
* @param string $ip IPv4 address. | |||
* @param boolean $mapped If true a Full IPv6 address is returned within the | |||
* official ipv4to6 mapped space "0:0:0:0:0:ffff:x:x" | |||
*/ | |||
public static function to_ipv6($ip, $mapped = false) | |||
{ | |||
if (!self::isIPv4($ip)) { | |||
throw new \InvalidArgumentException("Invalid IPv4 address \"$ip\""); | |||
} | |||
$num = IP::inet_ptod($ip); | |||
$o1 = dechex($num >> 16); | |||
$o2 = dechex($num & 0x0000FFFF); | |||
return $mapped ? "0:0:0:0:0:ffff:$o1:$o2" : "$o1:$o2"; | |||
} | |||
/** | |||
* Returns true if the IP address is a valid IPv4 address | |||
*/ | |||
public static function isIPv4($ip) | |||
{ | |||
return $ip === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); | |||
} | |||
/** | |||
* Returns true if the IP address is a valid IPv6 address | |||
*/ | |||
public static function isIPv6($ip) | |||
{ | |||
return $ip === filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); | |||
} | |||
/** | |||
* Compare two IP's (v4 or v6) and return -1, 0, 1 if the first is < = > | |||
* the second. | |||
* | |||
* @param string $ip1 IP address | |||
* @param string $ip2 IP address to compare against | |||
* @return integer Return -1,0,1 depending if $ip1 is <=> $ip2 | |||
*/ | |||
public static function cmp($ip1, $ip2) | |||
{ | |||
return bccomp(self::inet_ptod($ip1), self::inet_ptod($ip2), 0); | |||
} | |||
} |
@@ -1,15 +0,0 @@ | |||
<?php | |||
/* | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
// WARNING: Including this file is DEPRECIATED. It's only here to support older versions and won't exist forever. | |||
if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { | |||
// You cannot request this file directly. | |||
exit; | |||
} | |||
require 'inc/mod/auth.php'; | |||
@@ -4,10 +4,7 @@ | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { | |||
// You cannot request this file directly. | |||
exit; | |||
} | |||
defined('TINYBOARD') or exit; | |||
// create a hash/salt pair for validate logins | |||
function mkhash($username, $password, $salt = false) { | |||
@@ -4,102 +4,7 @@ | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { | |||
// You cannot request this file directly. | |||
exit; | |||
} | |||
defined('TINYBOARD') or exit; | |||
function parse_time($str) { | |||
if (empty($str)) | |||
return false; | |||
if (($time = @strtotime($str)) !== false) | |||
return $time; | |||
if (!preg_match('/^((\d+)\s?ye?a?r?s?)?\s?+((\d+)\s?mon?t?h?s?)?\s?+((\d+)\s?we?e?k?s?)?\s?+((\d+)\s?da?y?s?)?((\d+)\s?ho?u?r?s?)?\s?+((\d+)\s?mi?n?u?t?e?s?)?\s?+((\d+)\s?se?c?o?n?d?s?)?$/', $str, $matches)) | |||
return false; | |||
$expire = 0; | |||
if (isset($matches[2])) { | |||
// Years | |||
$expire += $matches[2]*60*60*24*365; | |||
} | |||
if (isset($matches[4])) { | |||
// Months | |||
$expire += $matches[4]*60*60*24*30; | |||
} | |||
if (isset($matches[6])) { | |||
// Weeks | |||
$expire += $matches[6]*60*60*24*7; | |||
} | |||
if (isset($matches[8])) { | |||
// Days | |||
$expire += $matches[8]*60*60*24; | |||
} | |||
if (isset($matches[10])) { | |||
// Hours | |||
$expire += $matches[10]*60*60; | |||
} | |||
if (isset($matches[12])) { | |||
// Minutes | |||
$expire += $matches[12]*60; | |||
} | |||
if (isset($matches[14])) { | |||
// Seconds | |||
$expire += $matches[14]; | |||
} | |||
return time() + $expire; | |||
} | |||
function ban($mask, $reason, $length, $board) { | |||
global $mod, $pdo; | |||
$query = prepare("INSERT INTO ``bans`` VALUES (NULL, :ip, :mod, :time, :expires, :reason, :board, 0)"); | |||
$query->bindValue(':ip', $mask); | |||
$query->bindValue(':mod', $mod['id']); | |||
$query->bindValue(':time', time()); | |||
if ($reason !== '') { | |||
$reason = escape_markup_modifiers($reason); | |||
markup($reason); | |||
$query->bindValue(':reason', $reason); | |||
} else | |||
$query->bindValue(':reason', null, PDO::PARAM_NULL); | |||
if ($length > 0) | |||
$query->bindValue(':expires', $length); | |||
else | |||
$query->bindValue(':expires', null, PDO::PARAM_NULL); | |||
if ($board) | |||
$query->bindValue(':board', $board); | |||
else | |||
$query->bindValue(':board', null, PDO::PARAM_NULL); | |||
$query->execute() or error(db_error($query)); | |||
modLog('Created a new ' . | |||
($length > 0 ? preg_replace('/^(\d+) (\w+?)s?$/', '$1-$2', until($length)) : 'permanent') . | |||
' ban on ' . | |||
($board ? '/' . $board . '/' : 'all boards') . | |||
' for ' . | |||
(filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$mask\">$mask</a>" : utf8tohtml($mask)) . | |||
' (<small>#' . $pdo->lastInsertId() . '</small>)' . | |||
' with ' . ($reason ? 'reason: ' . utf8tohtml($reason) . '' : 'no reason')); | |||
} | |||
function unban($id) { | |||
$query = prepare("SELECT `ip` FROM ``bans`` WHERE `id` = :id"); | |||
$query->bindValue(':id', $id); | |||
$query->execute() or error(db_error($query)); | |||
$mask = $query->fetchColumn(); | |||
$query = prepare("DELETE FROM ``bans`` WHERE `id` = :id"); | |||
$query->bindValue(':id', $id); | |||
$query->execute() or error(db_error($query)); | |||
if ($mask) | |||
modLog("Removed ban #{$id} for " . (filter_var($mask, FILTER_VALIDATE_IP) !== false ? "<a href=\"?/IP/$mask\">$mask</a>" : utf8tohtml($mask))); | |||
} | |||
// This file is no longer used. |
@@ -1,9 +1,15 @@ | |||
<?php | |||
/* | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
defined('TINYBOARD') or exit; | |||
function permission_to_edit_config_var($varname) { | |||
global $config, $mod; | |||
if (is_array($config['mod']['config'][DISABLED])) { | |||
if (isset($config['mod']['config'][DISABLED])) { | |||
foreach ($config['mod']['config'][DISABLED] as $disabled_var_name) { | |||
$disabled_var_name = explode('>', $disabled_var_name); | |||
if (count($disabled_var_name) == 1) | |||
@@ -14,10 +20,11 @@ function permission_to_edit_config_var($varname) { | |||
} | |||
$allow_only = false; | |||
// for ($perm = (int)$mod['type']; $perm >= JANITOR; $perm --) { | |||
for ($perm = JANITOR; $perm <= (int)$mod['type']; $perm ++) { | |||
foreach ($config['mod']['groups'] as $perm => $perm_name) { | |||
if ($perm > $mod['type']) | |||
break; | |||
$allow_only = false; | |||
if (is_array($config['mod']['config'][$perm])) { | |||
if (isset($config['mod']['config'][$perm]) && is_array($config['mod']['config'][$perm])) { | |||
foreach ($config['mod']['config'][$perm] as $perm_var_name) { | |||
if ($perm_var_name == '!') { | |||
$allow_only = true; | |||
@@ -92,7 +99,7 @@ function config_vars() { | |||
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'] . ';'); | |||
$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)) { | |||
@@ -4,10 +4,7 @@ | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { | |||
// You cannot request this file directly. | |||
exit; | |||
} | |||
defined('TINYBOARD') or exit; | |||
function mod_page($title, $template, $args, $subtitle = false) { | |||
global $config, $mod; | |||
@@ -18,6 +15,7 @@ function mod_page($title, $template, $args, $subtitle = false) { | |||
'hide_dashboard_link' => $template == 'mod/dashboard.html', | |||
'title' => $title, | |||
'subtitle' => $subtitle, | |||
'nojavascript' => true, | |||
'body' => Element($template, | |||
array_merge( | |||
array('config' => $config, 'mod' => $mod), | |||
@@ -291,7 +289,7 @@ function mod_search($type, $search_query_escaped, $page_no = 1) { | |||
} | |||
if ($type == 'bans') { | |||
$query = 'SELECT ``bans``.*, `username` FROM ``bans`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE ' . $sql_like . ' ORDER BY (`expires` IS NOT NULL AND `expires` < UNIX_TIMESTAMP()), `set` DESC'; | |||
$query = 'SELECT ``bans``.*, `username` FROM ``bans`` LEFT JOIN ``mods`` ON `creator` = ``mods``.`id` WHERE ' . $sql_like . ' ORDER BY (`expires` IS NOT NULL AND `expires` < UNIX_TIMESTAMP()), `created` DESC'; | |||
$sql_table = 'bans'; | |||
if (!hasPermission($config['mod']['view_banlist'])) | |||
error($config['error']['noaccess']); | |||
@@ -319,8 +317,9 @@ function mod_search($type, $search_query_escaped, $page_no = 1) { | |||
if ($type == 'bans') { | |||
foreach ($results as &$ban) { | |||
if (filter_var($ban['ip'], FILTER_VALIDATE_IP) !== false) | |||
$ban['real_ip'] = true; | |||
$ban['mask'] = Bans::range_to_string(array($ban['ipstart'], $ban['ipend'])); | |||
if (filter_var($ban['mask'], FILTER_VALIDATE_IP) !== false) | |||
$ban['single_addr'] = true; | |||
} | |||
} | |||
@@ -752,9 +751,7 @@ function mod_page_ip($ip) { | |||
if (!hasPermission($config['mod']['unban'])) | |||
error($config['error']['noaccess']); | |||
require_once 'inc/mod/ban.php'; | |||
unban($_POST['ban_id']); | |||
Bans::delete($_POST['ban_id'], true); | |||
header('Location: ?/IP/' . $ip . '#bans', true, $config['redirect_http']); | |||
return; | |||
@@ -813,10 +810,7 @@ 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 ORDER BY `set` DESC"); | |||
$query->bindValue(':ip', $ip); | |||
$query->execute() or error(db_error($query)); | |||
$args['bans'] = $query->fetchAll(PDO::FETCH_ASSOC); | |||
$args['bans'] = Bans::find($ip, false, true); | |||
} | |||
if (hasPermission($config['mod']['view_notes'])) { | |||
@@ -851,7 +845,7 @@ function mod_ban() { | |||
require_once 'inc/mod/ban.php'; | |||
ban($_POST['ip'], $_POST['reason'], parse_time($_POST['length']), $_POST['board'] == '*' ? false : $_POST['board']); | |||
Bans::new_ban($_POST['ip'], $_POST['reason'], $_POST['length'], $_POST['board'] == '*' ? false : $_POST['board']); | |||
if (isset($_POST['redirect'])) | |||
header('Location: ' . $_POST['redirect'], true, $config['redirect_http']); | |||
@@ -877,58 +871,27 @@ function mod_bans($page_no = 1) { | |||
if (preg_match('/^ban_(\d+)$/', $name, $match)) | |||
$unban[] = $match[1]; | |||
} | |||
if (isset($config['mod']['unban_limit'])){ | |||
if (count($unban) <= $config['mod']['unban_limit'] || $config['mod']['unban_limit'] == -1){ | |||
if (!empty($unban)) { | |||
query('DELETE FROM ``bans`` WHERE `id` = ' . implode(' OR `id` = ', $unban)) or error(db_error()); | |||
if (isset($config['mod']['unban_limit']) && $config['mod']['unban_limit'] && count($unban) > $config['mod']['unban_limit']) | |||
error(sprintf($config['error']['toomanyunban'], $config['mod']['unban_limit'], count($unban))); | |||
foreach ($unban as $id) { | |||
modLog("Removed ban #{$id}"); | |||
} | |||
} | |||
} else { | |||
error(sprintf($config['error']['toomanyunban'], $config['mod']['unban_limit'], count($unban) )); | |||
foreach ($unban as $id) { | |||
Bans::delete($id, true); | |||
} | |||
} else { | |||
if (!empty($unban)) { | |||
query('DELETE FROM ``bans`` WHERE `id` = ' . implode(' OR `id` = ', $unban)) or error(db_error()); | |||
foreach ($unban as $id) { | |||
modLog("Removed ban #{$id}"); | |||
} | |||
} | |||
} | |||
header('Location: ?/bans', true, $config['redirect_http']); | |||
return; | |||
} | |||
if ($config['mod']['view_banexpired']) { | |||
$query = prepare("SELECT ``bans``.*, `username` FROM ``bans`` LEFT JOIN ``mods`` ON `mod` = ``mods``.`id` ORDER BY (`expires` IS NOT NULL AND `expires` < :time), `set` DESC LIMIT :offset, :limit"); | |||
} else { | |||
// Filter out expired bans | |||
$query = prepare("SELECT ``bans``.*, `username` FROM ``bans`` INNER JOIN ``mods`` ON `mod` = ``mods``.`id` WHERE `expires` = 0 OR `expires` > :time ORDER BY `set` DESC LIMIT :offset, :limit"); | |||
} | |||
$query->bindValue(':time', time(), PDO::PARAM_INT); | |||
$query->bindValue(':limit', $config['mod']['banlist_page'], PDO::PARAM_INT); | |||
$query->bindValue(':offset', ($page_no - 1) * $config['mod']['banlist_page'], PDO::PARAM_INT); | |||
$query->execute() or error(db_error($query)); | |||
$bans = $query->fetchAll(PDO::FETCH_ASSOC); | |||
$bans = Bans::list_all(($page_no - 1) * $config['mod']['banlist_page'], $config['mod']['banlist_page']); | |||
if (empty($bans) && $page_no > 1) | |||
error($config['error']['404']); | |||
$query = prepare("SELECT COUNT(*) FROM ``bans``"); | |||
$query->execute() or error(db_error($query)); | |||
$count = $query->fetchColumn(); | |||
foreach ($bans as &$ban) { | |||
if (filter_var($ban['ip'], FILTER_VALIDATE_IP) !== false) | |||
$ban['real_ip'] = true; | |||
if (filter_var($ban['mask'], FILTER_VALIDATE_IP) !== false) | |||
$ban['single_addr'] = true; | |||
} | |||
mod_page(_('Ban list'), 'mod/ban_list.html', array('bans' => $bans, 'count' => $count)); | |||
mod_page(_('Ban list'), 'mod/ban_list.html', array('bans' => $bans, 'count' => Bans::count())); | |||
} | |||
@@ -1327,7 +1290,7 @@ function mod_ban_post($board, $delete, $post, $token = false) { | |||
if (isset($_POST['ip'])) | |||
$ip = $_POST['ip']; | |||
ban($ip, $_POST['reason'], parse_time($_POST['length']), $_POST['board'] == '*' ? false : $_POST['board']); | |||
Bans::new_ban($_POST['ip'], $_POST['reason'], $_POST['length'], $_POST['board'] == '*' ? false : $_POST['board']); | |||
if (isset($_POST['public_message'], $_POST['message'])) { | |||
// public ban message | |||
@@ -1740,8 +1703,8 @@ function mod_user_new() { | |||
} | |||
} | |||
$_POST['type'] = (int) $_POST['type']; | |||
if ($_POST['type'] !== JANITOR && $_POST['type'] !== MOD && $_POST['type'] !== ADMIN) | |||
$type = (int)$_POST['type']; | |||
if (!isset($config['mod']['groups'][$type]) || $type == DISABLED) | |||
error(sprintf($config['error']['invalidfield'], 'type')); | |||
$salt = generate_salt(); | |||
@@ -1751,7 +1714,7 @@ function mod_user_new() { | |||
$query->bindValue(':username', $_POST['username']); | |||
$query->bindValue(':password', $password); | |||
$query->bindValue(':salt', $salt); | |||
$query->bindValue(':type', $_POST['type']); | |||
$query->bindValue(':type', $type); | |||
$query->bindValue(':boards', implode(',', $boards)); | |||
$query->execute() or error(db_error($query)); | |||
@@ -1785,11 +1748,39 @@ function mod_user_promote($uid, $action) { | |||
if (!hasPermission($config['mod']['promoteusers'])) | |||
error($config['error']['noaccess']); | |||
$query = prepare("UPDATE ``mods`` SET `type` = `type` " . ($action == 'promote' ? "+1 WHERE `type` < " . (int)ADMIN : "-1 WHERE `type` > " . (int)JANITOR) . " AND `id` = :id"); | |||
$query = prepare("SELECT `type`, `username` FROM ``mods`` WHERE `id` = :id"); | |||
$query->bindValue(':id', $uid); | |||
$query->execute() or error(db_error($query)); | |||
modLog(($action == 'promote' ? 'Promoted' : 'Demoted') . " user #{$uid}"); | |||
if (!$mod = $query->fetch(PDO::FETCH_ASSOC)) | |||
error($config['error']['404']); | |||
$new_group = false; | |||
$groups = $config['mod']['groups']; | |||
if ($action == 'demote') | |||
$groups = array_reverse($groups, true); | |||
foreach ($groups as $group_value => $group_name) { | |||
if ($action == 'promote' && $group_value > $mod['type']) { | |||
$new_group = $group_value; | |||
break; | |||
} elseif ($action == 'demote' && $group_value < $mod['type']) { | |||
$new_group = $group_value; | |||
break; | |||
} | |||
} | |||
if ($new_group === false || $new_group == DISABLED) | |||
error(_('Impossible to promote/demote user.')); | |||
$query = prepare("UPDATE ``mods`` SET `type` = :group_value WHERE `id` = :id"); | |||
$query->bindValue(':id', $uid); | |||
$query->bindValue(':group_value', $new_group); | |||
$query->execute() or error(db_error($query)); | |||
modLog(($action == 'promote' ? 'Promoted' : 'Demoted') . ' user "' . | |||
utf8tohtml($mod['username']) . '" to ' . $config['mod']['groups'][$new_group]); | |||
header('Location: ?/users', true, $config['redirect_http']); | |||
} | |||
@@ -1922,7 +1913,7 @@ function mod_rebuild() { | |||
if (isset($_POST['rebuild'])) { | |||
@set_time_limit($config['mod']['rebuild_timelimit']); | |||
$log = array(); | |||
$boards = listBoards(); | |||
$rebuilt_scripts = array(); | |||
@@ -1954,6 +1945,7 @@ function mod_rebuild() { | |||
continue; | |||
openBoard($board['uri']); | |||
$config['try_smarter'] = false; | |||
if (isset($_POST['rebuild_index'])) { | |||
buildIndex(); | |||
@@ -2180,14 +2172,8 @@ function mod_config($board_config = false) { | |||
$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]; | |||
if (@$var['permissions'] && isset($config['mod']['groups'][$value])) { | |||
$config_append .= $config['mod']['groups'][$value]; | |||
} else { | |||
$config_append .= var_export($value, true); | |||
} | |||
@@ -2417,11 +2403,21 @@ function mod_debug_recent_posts() { | |||
$query = query($query) or error(db_error()); | |||
$posts = $query->fetchAll(PDO::FETCH_ASSOC); | |||
// Fetch recent posts from flood prevention cache | |||
$query = query("SELECT * FROM ``flood`` ORDER BY `time` DESC") or error(db_error()); | |||
$flood_posts = $query->fetchAll(PDO::FETCH_ASSOC); | |||
foreach ($posts as &$post) { | |||
$post['snippet'] = pm_snippet($post['body']); | |||
foreach ($flood_posts as $flood_post) { | |||
if ($flood_post['time'] == $post['time'] && | |||
$flood_post['posthash'] == make_comment_hex($post['body_nomarkup']) && | |||
$flood_post['filehash'] == $post['filehash']) | |||
$post['in_flood_table'] = true; | |||
} | |||
} | |||
mod_page(_('Debug: Recent posts'), 'mod/debug/recent_posts.html', array('posts' => $posts)); | |||
mod_page(_('Debug: Recent posts'), 'mod/debug/recent_posts.html', array('posts' => $posts, 'flood_posts' => $flood_posts)); | |||
} | |||
function mod_debug_sql() { | |||
@@ -4,10 +4,7 @@ | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { | |||
// You cannot request this file directly. | |||
exit; | |||
} | |||
defined('TINYBOARD') or exit; | |||
class Remote { | |||
public function __construct($config) { | |||
@@ -4,10 +4,7 @@ | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
if (realpath($_SERVER['SCRIPT_FILENAME']) == str_replace('\\', '/', __FILE__)) { | |||
// You cannot request this file directly. | |||
exit; | |||
} | |||
defined('TINYBOARD') or exit; | |||
$twig = false; | |||
@@ -1,7 +1,7 @@ | |||
<?php | |||
// Installation/upgrade file | |||
define('VERSION', 'v0.9.6-dev-16 + <a href="https://int.vichan.net/devel/">vichan-devel-4.0.13</a>'); | |||
define('VERSION', 'v0.9.6-dev-21 + <a href="https://int.vichan.net/devel/">vichan-devel-4.4.90</a>'); | |||
require 'inc/functions.php'; | |||
@@ -401,6 +401,93 @@ if (file_exists($config['has_installed'])) { | |||
ADD INDEX `list_threads` (`thread`, `sticky`, `bump`)", $board['uri'])) or error(db_error()); | |||
} | |||
case 'v0.9.6-dev-16': | |||
case 'v0.9.6-dev-16 + <a href="https://int.vichan.net/devel/">vichan-devel-4.0.13</a>': | |||
query("ALTER TABLE ``bans`` ADD INDEX `seen` (`seen`)") or error(db_error()); | |||
case 'v0.9.6-dev-17': | |||
query("ALTER TABLE ``ip_notes`` | |||
DROP INDEX `ip`, | |||
ADD INDEX `ip_lookup` (`ip`, `time`)") or error(db_error()); | |||
case 'v0.9.6-dev-18': | |||
query("CREATE TABLE IF NOT EXISTS ``flood`` ( | |||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT, | |||
`ip` varchar(39) NOT NULL, | |||
`board` varchar(58) CHARACTER SET utf8 NOT NULL, | |||
`time` int(11) NOT NULL, | |||
`posthash` char(32) NOT NULL, | |||
`filehash` char(32) DEFAULT NULL, | |||
`isreply` tinyint(1) NOT NULL, | |||
PRIMARY KEY (`id`), | |||
KEY `ip` (`ip`), | |||
KEY `posthash` (`posthash`), | |||
KEY `filehash` (`filehash`), | |||
KEY `time` (`time`) | |||
) ENGINE=MyISAM DEFAULT CHARSET=ascii COLLATE=ascii_bin AUTO_INCREMENT=1 ;") or error(db_error()); | |||
case 'v0.9.6-dev-19': | |||
query("UPDATE ``mods`` SET `type` = 10 WHERE `type` = 0") or error(db_error()); | |||
query("UPDATE ``mods`` SET `type` = 20 WHERE `type` = 1") or error(db_error()); | |||
query("UPDATE ``mods`` SET `type` = 30 WHERE `type` = 2") or error(db_error()); | |||
query("ALTER TABLE ``mods`` CHANGE `type` `type` smallint(1) NOT NULL") or error(db_error()); | |||
case 'v0.9.6-dev-20': | |||
query("CREATE TABLE IF NOT EXISTS `bans_new_temp` ( | |||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, | |||
`ipstart` varbinary(16) NOT NULL, | |||
`ipend` varbinary(16) DEFAULT NULL, | |||
`created` int(10) unsigned NOT NULL, | |||
`expires` int(10) unsigned DEFAULT NULL, | |||
`board` varchar(58) DEFAULT NULL, | |||
`creator` int(10) NOT NULL, | |||
`reason` text, | |||
`seen` tinyint(1) NOT NULL, | |||
`post` blob, | |||
PRIMARY KEY (`id`), | |||
KEY `expires` (`expires`), | |||
KEY `ipstart` (`ipstart`,`ipend`) | |||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1") or error(db_error()); | |||
$listquery = query("SELECT * FROM ``bans`` ORDER BY `id`") or error(db_error()); | |||
while ($ban = $listquery->fetch(PDO::FETCH_ASSOC)) { | |||
$query = prepare("INSERT INTO ``bans_new_temp`` VALUES | |||
(NULL, :ipstart, :ipend, :created, :expires, :board, :creator, :reason, :seen, NULL)"); | |||
$range = Bans::parse_range($ban['ip']); | |||
if ($range === false) { | |||
// Invalid retard ban; just skip it. | |||
continue; | |||
} | |||
$query->bindValue(':ipstart', $range[0]); | |||
if ($range[1] !== false && $range[1] != $range[0]) | |||
$query->bindValue(':ipend', $range[1]); | |||
else | |||
$query->bindValue(':ipend', null, PDO::PARAM_NULL); | |||
$query->bindValue(':created', $ban['set']); | |||
if ($ban['expires']) | |||
$query->bindValue(':expires', $ban['expires']); | |||
else | |||
$query->bindValue(':expires', null, PDO::PARAM_NULL); | |||
if ($ban['board']) | |||
$query->bindValue(':board', $ban['board']); | |||
else | |||
$query->bindValue(':board', null, PDO::PARAM_NULL); | |||
$query->bindValue(':creator', $ban['mod']); | |||
if ($ban['reason']) | |||
$query->bindValue(':reason', $ban['reason']); | |||
else | |||
$query->bindValue(':reason', null, PDO::PARAM_NULL); | |||
$query->bindValue(':seen', $ban['seen']); | |||
$query->execute() or error(db_error($query)); | |||
} | |||
// Drop old bans table | |||
query("DROP TABLE ``bans``") or error(db_error()); | |||
// Replace with new table | |||
query("RENAME TABLE ``bans_new_temp`` TO ``bans``") or error(db_error()); | |||
case 'v0.9.6-dev-21': | |||
case false: | |||
// Update version number | |||
file_write($config['has_installed'], VERSION); | |||
@@ -40,17 +40,20 @@ CREATE TABLE IF NOT EXISTS `antispam` ( | |||
-- | |||
CREATE TABLE IF NOT EXISTS `bans` ( | |||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT, | |||
`ip` varchar(39) CHARACTER SET ascii NOT NULL, | |||
`mod` int(11) NOT NULL COMMENT 'which mod made the ban', | |||
`set` int(11) NOT NULL, | |||
`expires` int(11) DEFAULT NULL, | |||
`id` int(10) unsigned NOT NULL AUTO_INCREMENT, | |||
`ipstart` varbinary(16) NOT NULL, | |||
`ipend` varbinary(16) DEFAULT NULL, | |||
`created` int(10) unsigned NOT NULL, | |||
`expires` int(10) unsigned DEFAULT NULL, | |||
`board` varchar(58) DEFAULT NULL, | |||
`creator` int(10) NOT NULL, | |||
`reason` text, | |||
`board` varchar(58) CHARACTER SET utf8 DEFAULT NULL, | |||
`seen` tinyint(1) NOT NULL, | |||
`post` blob, | |||
PRIMARY KEY (`id`), | |||
KEY `ip` (`ip`) | |||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ; | |||
KEY `expires` (`expires`), | |||
KEY `ipstart` (`ipstart`,`ipend`) | |||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ; | |||
-- -------------------------------------------------------- | |||
@@ -100,7 +103,7 @@ CREATE TABLE IF NOT EXISTS `ip_notes` ( | |||
`time` int(11) NOT NULL, | |||
`body` text NOT NULL, | |||
UNIQUE KEY `id` (`id`), | |||
KEY `ip` (`ip`) | |||
KEY `ip_lookup` (`ip`, `time`) | |||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ; | |||
-- -------------------------------------------------------- | |||
@@ -130,18 +133,18 @@ CREATE TABLE IF NOT EXISTS `mods` ( | |||
`username` varchar(30) NOT NULL, | |||
`password` char(64) CHARACTER SET ascii NOT NULL COMMENT 'SHA256', | |||
`salt` char(32) CHARACTER SET ascii NOT NULL, | |||
`type` smallint(1) NOT NULL COMMENT '0: janitor, 1: mod, 2: admin', | |||
`type` smallint(2) NOT NULL, | |||
`boards` text CHARACTER SET utf8 NOT NULL, | |||
PRIMARY KEY (`id`), | |||
UNIQUE KEY `id` (`id`,`username`) | |||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=5 ; | |||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 AUTO_INCREMENT=1 ; | |||
-- | |||
-- Dumping data for table `mods` | |||
-- | |||
INSERT INTO `mods` VALUES | |||
(1, 'admin', 'cedad442efeef7112fed0f50b011b2b9bf83f6898082f995f69dd7865ca19fb7', '4a44c6c55df862ae901b413feecb0d49', 2, '*'); | |||
(1, 'admin', 'cedad442efeef7112fed0f50b011b2b9bf83f6898082f995f69dd7865ca19fb7', '4a44c6c55df862ae901b413feecb0d49', 30, '*'); | |||
-- -------------------------------------------------------- | |||
@@ -256,6 +259,27 @@ CREATE TABLE IF NOT EXISTS `theme_settings` ( | |||
KEY `theme` (`theme`) | |||
) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; | |||
-- -------------------------------------------------------- | |||
-- | |||
-- Table structure for table `flood` | |||
-- | |||
CREATE TABLE IF NOT EXISTS `flood` ( | |||
`id` int(11) unsigned NOT NULL AUTO_INCREMENT, | |||
`ip` varchar(39) NOT NULL, | |||
`board` varchar(58) CHARACTER SET utf8 NOT NULL, | |||
`time` int(11) NOT NULL, | |||
`posthash` char(32) NOT NULL, | |||
`filehash` char(32) DEFAULT NULL, | |||
`isreply` tinyint(1) NOT NULL, | |||
PRIMARY KEY (`id`), | |||
KEY `ip` (`ip`), | |||
KEY `posthash` (`posthash`), | |||
KEY `filehash` (`filehash`), | |||
KEY `time` (`time`) | |||
) ENGINE=MyISAM DEFAULT CHARSET=ascii COLLATE=ascii_bin AUTO_INCREMENT=1 ; | |||
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; | |||
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; | |||
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; |
@@ -0,0 +1,77 @@ | |||
/* | |||
* ajax-post-controls.js | |||
* https://github.com/savetheinternet/Tinyboard/blob/master/js/ajax-post-controls.js | |||
* | |||
* Released under the MIT license | |||
* Copyright (c) 2013 Michael Save <savetheinternet@tinyboard.org> | |||
* | |||
* Usage: | |||
* $config['additional_javascript'][] = 'js/jquery.min.js'; | |||
* $config['additional_javascript'][] = 'js/ajax-post-controls.js'; | |||
* | |||
*/ | |||
$(window).ready(function() { | |||
var do_not_ajax = false; | |||
var setup_form = function($form) { | |||
$form.find('input[type="submit"]').click(function() { | |||
$form.data('submit-btn', this); | |||
});; | |||
$form.submit(function(e) { | |||
if (!$(this).data('submit-btn')) | |||
return true; | |||
if (do_not_ajax) | |||
return true; | |||
if (window.FormData === undefined) | |||
return true; | |||
var form = this; | |||
var formData = new FormData(this); | |||
formData.append('json_response', '1'); | |||
formData.append($($(form).data('submit-btn')).attr('name'), $($(form).data('submit-btn')).val()); | |||
$.ajax({ | |||
url: this.action, | |||
type: 'POST', | |||
success: function(post_response) { | |||
if (post_response.error) { | |||
alert(post_response.error); | |||
} else if (post_response.success) { | |||
if ($($(form).data('submit-btn')).attr('name') == 'report') { | |||
alert(_('Reported post(s).')); | |||
if ($(form).hasClass('post-actions')) { | |||
$(form).parents('div.post').find('input[type="checkbox"].delete').click(); | |||
} else { | |||
$(form).find('input[name="reason"]').val(''); | |||
} | |||
} else { | |||
window.location.reload(); | |||
} | |||
} else { | |||
alert(_('An unknown error occured!')); | |||
} | |||
$($(form).data('submit-btn')).val($($(form).data('submit-btn')).data('orig-val')).removeAttr('disabled'); | |||
}, | |||
error: function(xhr, status, er) { | |||
// An error occured | |||
// TODO | |||
alert(_('Something went wrong... An unknown error occured!')); | |||
}, | |||
data: formData, | |||
cache: false, | |||
contentType: false, | |||
processData: false | |||
}, 'json'); | |||
$($(form).data('submit-btn')).attr('disabled', true).data('orig-val', $($(form).data('submit-btn')).val()).val(_('Working...')); | |||
return false; | |||
}); | |||
}; | |||
setup_form($('form[name="postcontrols"]')); | |||
$(window).on('quick-post-controls', function(e, form) { | |||
setup_form($(form)); | |||
}); | |||
}); |
@@ -0,0 +1,128 @@ | |||
/* | |||
* ajax.js | |||
* https://github.com/savetheinternet/Tinyboard/blob/master/js/ajax.js | |||
* | |||
* Released under the MIT license | |||
* Copyright (c) 2013 Michael Save <savetheinternet@tinyboard.org> | |||
* | |||
* Usage: | |||
* $config['additional_javascript'][] = 'js/jquery.min.js'; | |||
* $config['additional_javascript'][] = 'js/ajax.js'; | |||
* | |||
*/ | |||
$(window).ready(function() { | |||
var do_not_ajax = false; | |||
var setup_form = function($form) { | |||
$form.submit(function() { | |||
if (do_not_ajax) | |||
return true; | |||
var form = this; | |||
var submit_txt = $(this).find('input[type="submit"]').val(); | |||
if (window.FormData === undefined) | |||
return true; | |||
var formData = new FormData(this); | |||
formData.append('json_response', '1'); | |||
formData.append('post', submit_txt); | |||
var updateProgress = function(e) { | |||
$(form).find('input[type="submit"]').val(_('Posting... (#%)').replace('#', Math.round(e.position / e.total * 100))); | |||
}; | |||
$.ajax({ | |||
url: this.action, | |||
type: 'POST', | |||
xhr: function() { | |||
var xhr = $.ajaxSettings.xhr(); | |||
if(xhr.upload) { | |||
xhr.upload.addEventListener('progress', updateProgress, false); | |||
} | |||
return xhr; | |||
}, | |||
success: function(post_response) { | |||
if (post_response.error) { | |||
if (post_response.banned) { | |||
// You are banned. Must post the form normally so the user can see the ban message. | |||
do_not_ajax = true; | |||
$(form).find('input[type="submit"]').each(function() { | |||
var $replacement = $('<input type="hidden">'); | |||
$replacement.attr('name', $(this).attr('name')); | |||
$replacement.val(submit_txt); | |||
$(this) | |||
.after($replacement) | |||
.replaceWith($('<input type="button">').val(submit_txt)); | |||
}); | |||
$(form).submit(); | |||
} else { | |||
alert(post_response.error); | |||
$(form).find('input[type="submit"]').val(submit_txt); | |||
$(form).find('input[type="submit"]').removeAttr('disabled'); | |||
} | |||
} else if (post_response.redirect && post_response.id) { | |||
if (!$(form).find('input[name="thread"]').length) { | |||
document.location = post_response.redirect; | |||
} else { | |||
$.ajax({ | |||
url: document.location, | |||
success: function(data) { | |||
$(data).find('div.post.reply').each(function() { | |||
var id = $(this).attr('id'); | |||
if($('#' + id).length == 0) { | |||
$(this).insertAfter($('div.post:last').next()).after('<br class="clear">'); | |||
$(document).trigger('new_post', this); | |||
} | |||
}); | |||
highlightReply(post_response.id); | |||
window.location.hash = post_response.id; | |||
$(window).scrollTop($('div.post#reply_' + post_response.id).offset().top); | |||
$(form).find('input[type="submit"]').val(submit_txt); | |||
$(form).find('input[type="submit"]').removeAttr('disabled'); | |||
$(form).find('input[name="subject"],input[name="file_url"],\ | |||
textarea[name="body"],input[type="file"]').val('').change(); | |||
}, | |||
cache: false, | |||
contentType: false, | |||
processData: false | |||
}, 'html'); | |||
} | |||
$(form).find('input[type="submit"]').val(_('Posted...')); | |||
} else { | |||
alert(_('An unknown error occured when posting!')); | |||
$(form).find('input[type="submit"]').val(submit_txt); | |||
$(form).find('input[type="submit"]').removeAttr('disabled'); | |||
} | |||
}, | |||
error: function(xhr, status, er) { | |||
// An error occured | |||
do_not_ajax = true; | |||
$(form).find('input[type="submit"]').each(function() { | |||
var $replacement = $('<input type="hidden">'); | |||
$replacement.attr('name', $(this).attr('name')); | |||
$replacement.val(submit_txt); | |||
$(this) | |||
.after($replacement) | |||
.replaceWith($('<input type="button">').val(submit_txt)); | |||
}); | |||
$(form).submit(); | |||
}, | |||
data: formData, | |||
cache: false, | |||
contentType: false, | |||
processData: false | |||
}, 'json'); | |||
$(form).find('input[type="submit"]').val(_('Posting...')); | |||
$(form).find('input[type="submit"]').attr('disabled', true); | |||
return false; | |||
}); | |||
}; | |||
setup_form($('form[name="post"]')); | |||
$(window).on('quick-reply', function() { | |||
setup_form($('form#quick-reply')); | |||
}); | |||
}); |
@@ -63,6 +63,8 @@ $(document).ready(function(){ | |||
post_form.appendTo($(this).parent().parent()); | |||
//post_form.insertBefore($(this)); | |||
} | |||
$(window).trigger('quick-post-controls', post_form); | |||
} else { | |||
var elm = $(this).parent().parent().find('form'); | |||
@@ -0,0 +1,46 @@ | |||
/* | |||
* quick-reply.js | |||
* https://github.com/savetheinternet/Tinyboard/blob/master/js/quick-reply.js | |||
* | |||
* Released under the MIT license | |||
* Copyright (c) 2012 Michael Save <savetheinternet@tinyboard.org> | |||
* | |||
* Usage: | |||
* $config['quick_reply'] = true; | |||
* $config['additional_javascript'][] = 'js/jquery.min.js'; | |||
* $config['additional_javascript'][] = 'js/quick-reply.js'; | |||
* | |||
*/ | |||
$(document).ready(function(){ | |||
if($('div.banner').length != 0) | |||
return; // not index | |||
txt_new_topic = $('form[name=post] input[type=submit]').val(); | |||
txt_new_reply = txt_new_topic == _('Submit') ? txt_new_topic : new_reply_string; | |||
undo_quick_reply = function() { | |||
$('div.banner').remove(); | |||
$('form[name=post] input[type=submit]').val(txt_new_topic); | |||
$('form[name=post] input[name=quick-reply]').remove(); | |||
} | |||
$('div.post.op').each(function() { | |||
var id = $(this).children('p.intro').children('a.post_no:eq(1)').text(); | |||
$('<a href="#">['+_("Quick reply")+']</a>').insertAfter($(this).children('p.intro').children('a:last')).click(function() { | |||
$('div.banner').remove(); | |||
$('<div class="banner">'+fmt(_("Posting mode: Replying to <small>>>{0}</small>"), [id])+' <a class="unimportant" onclick="undo_quick_reply()" href="javascript:void(0)">['+_("Return")+']</a></div>') | |||
.insertBefore('form[name=post]'); | |||
$('form[name=post] input[type=submit]').val(txt_new_reply); | |||
$('<input type="hidden" name="quick-reply" value="' + id + '">').appendTo($('form[name=post]')); | |||
$('form[name=post] textarea').select(); | |||
window.scrollTo(0, 0); | |||
return false; | |||
}); | |||
}); | |||
}); | |||
@@ -0,0 +1,47 @@ | |||
/* | |||
* quick-reply.js | |||
* https://github.com/savetheinternet/Tinyboard/blob/master/js/quick-reply.js | |||
* | |||
* Released under the MIT license | |||
* Copyright (c) 2012 Michael Save <savetheinternet@tinyboard.org> | |||
* | |||
* Usage: | |||
* $config['quick_reply'] = true; | |||
* $config['additional_javascript'][] = 'js/jquery.min.js'; | |||
* $config['additional_javascript'][] = 'js/quick-reply.js'; | |||
* | |||
*/ | |||
if (active_page == 'index') { | |||
$(document).ready(function(){ | |||
if($('div.banner').length != 0) | |||
return; // not index | |||
txt_new_topic = $('form[name=post] input[type=submit]').val(); | |||
txt_new_reply = txt_new_topic == _('Submit') ? txt_new_topic : new_reply_string; | |||
undo_quick_reply = function() { | |||
$('div.banner').remove(); | |||
$('form[name=post] input[type=submit]').val(txt_new_topic); | |||
$('form[name=post] input[name=quick-reply]').remove(); | |||
} | |||
$('div.post.op').each(function() { | |||
var id = $(this).children('p.intro').children('a.post_no:eq(1)').text(); | |||
$('<a href="#">['+_("Quick reply")+']</a>').insertAfter($(this).children('p.intro').children('a:last')).click(function() { | |||
$('div.banner').remove(); | |||
$('<div class="banner">'+fmt(_("Posting mode: Replying to <small>>>{0}</small>"), [id])+' <a class="unimportant" onclick="undo_quick_reply()" href="javascript:void(0)">['+_("Return")+']</a></div>') | |||
.insertBefore('form[name=post]'); | |||
$('form[name=post] input[type=submit]').val(txt_new_reply); | |||
$('<input type="hidden" name="quick-reply" value="' + id + '">').appendTo($('form[name=post]')); | |||
$('form[name=post] textarea').select(); | |||
window.scrollTo(0, 0); | |||
return false; | |||
}); | |||
}); | |||
}); | |||
} |
@@ -3,45 +3,363 @@ | |||
* https://github.com/savetheinternet/Tinyboard/blob/master/js/quick-reply.js | |||
* | |||
* Released under the MIT license | |||
* Copyright (c) 2012 Michael Save <savetheinternet@tinyboard.org> | |||
* Copyright (c) 2013 Michael Save <savetheinternet@tinyboard.org> | |||
* | |||
* Usage: | |||
* $config['quick_reply'] = true; | |||
* $config['additional_javascript'][] = 'js/jquery.min.js'; | |||
* $config['additional_javascript'][] = 'js/jquery-ui.custom.min.js'; // Optional; if you want the form to be draggable. | |||
* $config['additional_javascript'][] = 'js/quick-reply.js'; | |||
* | |||
*/ | |||
if (active_page == 'index') { | |||
$(document).ready(function(){ | |||
if($('div.banner').length != 0) | |||
return; // not index | |||
var do_css = function() { | |||
$('#quick-reply-css').remove(); | |||
txt_new_topic = $('form[name=post] input[type=submit]').val(); | |||
txt_new_reply = txt_new_topic == _('Submit') ? txt_new_topic : new_reply_string; | |||
// Find background of reply posts | |||
var dummy_reply = $('<div class="post reply"></div>').appendTo($('body')); | |||
var reply_background = dummy_reply.css('backgroundColor'); | |||
var reply_border_style = dummy_reply.css('borderStyle'); | |||
var reply_border_color = dummy_reply.css('borderColor'); | |||
var reply_border_width = dummy_reply.css('borderWidth'); | |||
dummy_reply.remove(); | |||
undo_quick_reply = function() { | |||
$('div.banner').remove(); | |||
$('form[name=post] input[type=submit]').val(txt_new_topic); | |||
$('form[name=post] input[name=quick-reply]').remove(); | |||
} | |||
$('<style type="text/css" id="quick-reply-css">\ | |||
#quick-reply {\ | |||
position: fixed;\ | |||
right: 0;\ | |||
top: 5%;\ | |||
float: right;\ | |||
display: block;\ | |||
padding: 0 0 0 0;\ | |||
width: 300px;\ | |||
}\ | |||
#quick-reply table {\ | |||
border-collapse: collapse;\ | |||
background: ' + reply_background + ';\ | |||
border-style: ' + reply_border_style + ';\ | |||
border-width: ' + reply_border_width + ';\ | |||
border-color: ' + reply_border_color + ';\ | |||
margin: 0;\ | |||
width: 100%;\ | |||
}\ | |||
#quick-reply tr td:nth-child(2) {\ | |||
white-space: nowrap;\ | |||
text-align: right;\ | |||
padding-right: 4px;\ | |||
}\ | |||
#quick-reply tr td:nth-child(2) input[type="submit"] {\ | |||
width: 100%;\ | |||
}\ | |||
#quick-reply th, #quick-reply td {\ | |||
margin: 0;\ | |||
padding: 0;\ | |||
}\ | |||
#quick-reply th {\ | |||
text-align: center;\ | |||
padding: 2px 0;\ | |||
border: 1px solid #222;\ | |||
}\ | |||
#quick-reply th .handle {\ | |||
float: left;\ | |||
width: 100%;\ | |||
display: inline-block;\ | |||
}\ | |||
#quick-reply th .close-btn {\ | |||
float: right;\ | |||
padding: 0 5px;\ | |||
}\ | |||
#quick-reply input[type="text"], #quick-reply select {\ | |||
width: 100%;\ | |||
padding: 2px;\ | |||
font-size: 10pt;\ | |||
box-sizing: border-box;\ | |||
-webkit-box-sizing:border-box;\ | |||
-moz-box-sizing: border-box;\ | |||
}\ | |||
#quick-reply textarea {\ | |||
width: 100%;\ | |||
box-sizing: border-box;\ | |||
-webkit-box-sizing:border-box;\ | |||
-moz-box-sizing: border-box;\ | |||
font-size: 10pt;\ | |||
resize: vertical;\ | |||
}\ | |||
#quick-reply input, #quick-reply select, #quick-reply textarea {\ | |||
margin: 0 0 1px 0;\ | |||
}\ | |||
#quick-reply input[type="file"] {\ | |||
padding: 5px 2px;\ | |||
}\ | |||
#quick-reply .nonsense {\ | |||
display: none;\ | |||
}\ | |||
#quick-reply td.submit {\ | |||
width: 1%;\ | |||
}\ | |||
#quick-reply td.recaptcha {\ | |||
text-align: center;\ | |||
padding: 0 0 1px 0;\ | |||
}\ | |||
#quick-reply td.recaptcha span {\ | |||
display: inline-block;\ | |||
width: 100%;\ | |||
background: white;\ | |||
border: 1px solid #ccc;\ | |||
cursor: pointer;\ | |||
}\ | |||
#quick-reply td.recaptcha-response {\ | |||
padding: 0 0 1px 0;\ | |||
}\ | |||
@media screen and (max-width: 800px) {\ | |||
#quick-reply {\ | |||
display: none !important;\ | |||
}\ | |||
}\ | |||
</style>').appendTo($('head')); | |||
}; | |||
var show_quick_reply = function(){ | |||
if($('div.banner').length == 0) | |||
return; | |||
if($('#quick-reply').length != 0) | |||
return; | |||
$('div.post.op').each(function() { | |||
var id = $(this).children('p.intro').children('a.post_no:eq(1)').text(); | |||
$('<a href="#">['+_("Quick reply")+']</a>').insertAfter($(this).children('p.intro').children('a:last')).click(function() { | |||
$('div.banner').remove(); | |||
$('<div class="banner">'+fmt(_("Posting mode: Replying to <small>>>{0}</small>"), [id])+' <a class="unimportant" onclick="undo_quick_reply()" href="javascript:void(0)">['+_("Return")+']</a></div>') | |||
.insertBefore('form[name=post]'); | |||
$('form[name=post] input[type=submit]').val(txt_new_reply); | |||
$('<input type="hidden" name="quick-reply" value="' + id + '">').appendTo($('form[name=post]')); | |||
$('form[name=post] textarea').select(); | |||
do_css(); | |||
var $postForm = $('form[name="post"]').clone(); | |||
$postForm.clone(); | |||
$dummyStuff = $('<div class="nonsense"></div>').appendTo($postForm); | |||
$postForm.find('table tr').each(function() { | |||
var $th = $(this).children('th:first'); | |||
var $td = $(this).children('td:first'); | |||
if ($th.length && $td.length) { | |||
$td.attr('colspan', 2); | |||
if ($td.find('input[type="text"]').length) { | |||
// Replace <th> with input placeholders | |||
$td.find('input[type="text"]') | |||
.removeAttr('size') | |||
.attr('placeholder', $th.clone().children().remove().end().text()); | |||
} | |||
// Move anti-spam nonsense and remove <th> | |||
$th.contents().filter(function() { | |||
return this.nodeType == 3; // Node.TEXT_NODE | |||
}).remove(); | |||
$th.contents().appendTo($dummyStuff); | |||
$th.remove(); | |||
if ($td.find('input[name="password"]').length) { | |||
// Hide password field | |||
$(this).hide(); | |||
} | |||
// Fix submit button | |||
if ($td.find('input[type="submit"]').length) { | |||
$td.removeAttr('colspan'); | |||
$('<td class="submit"></td>').append($td.find('input[type="submit"]')).insertAfter($td); | |||
} | |||
// reCAPTCHA | |||
if ($td.find('#recaptcha_widget_div').length) { | |||
// Just show the image, and have it interact with the real form. | |||
var $captchaimg = $td.find('#recaptcha_image img'); | |||
$captchaimg | |||
.removeAttr('id') | |||
.removeAttr('style') | |||
.addClass('recaptcha_image') | |||
.click(function() { | |||
$('#recaptcha_reload').click(); | |||
}); | |||
// When we get a new captcha... | |||
$('#recaptcha_response_field').focus(function() { | |||
if ($captchaimg.attr('src') != $('#recaptcha_image img').attr('src')) { | |||
$captchaimg.attr('src', $('#recaptcha_image img').attr('src')); | |||
$postForm.find('input[name="recaptcha_challenge_field"]').val($('#recaptcha_challenge_field').val()); | |||
$postForm.find('input[name="recaptcha_response_field"]').val('').focus(); | |||
} | |||
}); | |||
$postForm.submit(function() { | |||
setTimeout(function() { | |||
$('#recaptcha_reload').click(); | |||
}, 200); | |||
}); | |||
// Make a new row for the response text | |||
var $newRow = $('<tr><td class="recaptcha-response" colspan="2"></td></tr>'); | |||
$newRow.children().first().append( | |||
$td.find('input').removeAttr('style') | |||
); | |||
$newRow.find('#recaptcha_response_field') | |||
.removeAttr('id') | |||
.addClass('recaptcha_response_field') | |||
.attr('placeholder', $('#recaptcha_response_field').attr('placeholder')); | |||
$('#recaptcha_response_field').addClass('recaptcha_response_field') | |||
$td.replaceWith($('<td class="recaptcha" colspan="2"></td>').append($('<span></span>').append($captchaimg))); | |||
$newRow.insertAfter(this); | |||
} | |||
// Upload section | |||
if ($td.find('input[type="file"]').length) { | |||
if ($td.find('input[name="file_url"]').length) { | |||
$file_url = $td.find('input[name="file_url"]'); | |||
// Make a new row for it | |||
var $newRow = $('<tr><td colspan="2"></td></tr>'); | |||
$file_url.clone().attr('placeholder', _('Upload URL')).appendTo($newRow.find('td')); | |||
$file_url.parent().remove(); | |||
$newRow.insertBefore(this); | |||
$td.find('label').remove(); | |||
$td.contents().filter(function() { | |||
return this.nodeType == 3; // Node.TEXT_NODE | |||
}).remove(); | |||
$td.find('input[name="file_url"]').removeAttr('id'); | |||
} | |||
if ($(this).find('input[name="spoiler"]').length) { | |||
$td.removeAttr('colspan'); | |||
} | |||
} | |||
window.scrollTo(0, 0); | |||
// Remove mod controls, because it looks shit. | |||
if ($td.find('input[type="checkbox"]').length) { | |||
var tr = this; | |||
$td.find('input[type="checkbox"]').each(function() { | |||
if ($(this).attr('name') == 'spoiler') { | |||
$td.find('label').remove(); | |||
$(this).attr('id', 'q-spoiler-image'); | |||
$postForm.find('input[type="file"]').parent() | |||
.removeAttr('colspan') | |||
.after($('<td class="spoiler"></td>').append(this, ' ', $('<label for="q-spoiler-image">').text(_('Spoiler Image')))); | |||
} else { | |||
$(tr).remove(); | |||
} | |||
}); | |||
} | |||
return false; | |||
}); | |||
$td.find('small').hide(); | |||
} | |||
}); | |||
$postForm.find('textarea[name="body"]').removeAttr('id').removeAttr('cols').attr('placeholder', _('Comment')); | |||
$postForm.find('textarea:not([name="body"]),input[type="hidden"]').removeAttr('id').appendTo($dummyStuff); | |||
$postForm.find('br').remove(); | |||
$postForm.find('table').prepend('<tr><th colspan="2">\ | |||
<span class="handle">\ | |||
<a class="close-btn" href="javascript:void(0)">X</a>\ | |||
' + _('Quick Reply') + '\ | |||
</span>\ | |||
</th></tr>'); | |||
$postForm.attr('id', 'quick-reply'); | |||
$postForm.appendTo($('body')).hide(); | |||
$origPostForm = $('form[name="post"]:first'); | |||
// Synchronise body text with original post form | |||
$origPostForm.find('textarea[name="body"]').bind('change input propertychange', function() { | |||
$postForm.find('textarea[name="body"]').val($(this).val()); | |||
}); | |||
$postForm.find('textarea[name="body"]').bind('change input propertychange', function() { | |||
$origPostForm.find('textarea[name="body"]').val($(this).val()); | |||
}); | |||
$postForm.find('textarea[name="body"]').focus(function() { | |||
$origPostForm.find('textarea[name="body"]').removeAttr('id'); | |||
$(this).attr('id', 'body'); | |||
}); | |||
$origPostForm.find('textarea[name="body"]').focus(function() { | |||
$postForm.find('textarea[name="body"]').removeAttr('id'); | |||
$(this).attr('id', 'body'); | |||
}); | |||
// Synchronise other inputs | |||
$origPostForm.find('input[type="text"],select').bind('change input propertychange', function() { | |||
$postForm.find('[name="' + $(this).attr('name') + '"]').val($(this).val()); | |||
}); | |||
$postForm.find('input[type="text"],select').bind('change input propertychange', function() { | |||
$origPostForm.find('[name="' + $(this).attr('name') + '"]').val($(this).val()); | |||
}); | |||
if (typeof $postForm.draggable != 'undefined') { | |||
if (localStorage.quickReplyPosition) { | |||
var offset = JSON.parse(localStorage.quickReplyPosition); | |||
if (offset.right > $(window).width() - $postForm.width()) | |||
offset.right = $(window).width() - $postForm.width(); | |||
if (offset.top > $(window).height() - $postForm.height()) | |||
offset.top = $(window).height() - $postForm.height(); | |||
$postForm.css('right', offset.right).css('top', offset.top); | |||
} | |||
$postForm.draggable({ | |||
handle: 'th .handle', | |||
containment: 'window', | |||
distance: 10, | |||
scroll: false, | |||
stop: function() { | |||
var offset = { | |||
top: $(this).offset().top - $(window).scrollTop(), | |||
right: $(window).width() - $(this).offset().left - $(this).width(), | |||
}; | |||
localStorage.quickReplyPosition = JSON.stringify(offset); | |||
$postForm.css('right', offset.right).css('top', offset.top).css('left', 'auto'); | |||
} | |||
}); | |||
$postForm.find('th .handle').css('cursor', 'move'); | |||
} | |||
$postForm.find('th .close-btn').click(function() { | |||
$origPostForm.find('textarea[name="body"]').attr('id', 'body'); | |||
$postForm.remove(); | |||
}); | |||
// Fix bug when table gets too big for form. Shouldn't exist, but crappy CSS etc. | |||
$postForm.show(); | |||
$postForm.width($postForm.find('table').width()); | |||
$postForm.hide(); | |||
$(window).trigger('quick-reply'); | |||
$(window).ready(function() { | |||
$(window).scroll(function() { | |||
if ($(this).width() <= 800) | |||
return; | |||
if ($(this).scrollTop() < $origPostForm.offset().top + $origPostForm.height() - 100) | |||
$postForm.fadeOut(100); | |||
else | |||
$postForm.fadeIn(100); | |||
}).on('stylesheet', function() { | |||
do_css(); | |||
if ($('link#stylesheet').attr('href')) { | |||
$('link#stylesheet')[0].onload = do_css; | |||
} | |||
}).scroll(); | |||
}); | |||
}; | |||
$(window).on('cite', function(e, id, with_link) { | |||
if ($(this).width() <= 800) | |||
return; | |||
show_quick_reply(); | |||
$('#quick-reply textarea').focus(); | |||
if (with_link) { | |||
$(window).ready(function() { | |||
if ($('#' + id).length) { | |||
highlightReply(id); | |||
$(window).scrollTop($('#' + id).offset().top); | |||
} | |||
}); | |||
} | |||
}); | |||
} |
@@ -1,5 +1,7 @@ | |||
<?php | |||
file_put_contents('post.txt', var_export($_POST, true)); | |||
/* | |||
* Copyright (c) 2010-2013 Tinyboard Development Group | |||
*/ | |||
@@ -56,7 +58,7 @@ if (isset($_POST['delete'])) { | |||
if ($password != '' && $post['password'] != $password) | |||
error($config['error']['invalidpassword']); | |||
if ($post['time'] >= time() - $config['delete_time']) { | |||
if ($post['time'] > time() - $config['delete_time']) { | |||
error(sprintf($config['error']['delete_too_soon'], until($post['time'] + $config['delete_time']))); | |||
} | |||
@@ -82,9 +84,12 @@ if (isset($_POST['delete'])) { | |||
$is_mod = isset($_POST['mod']) && $_POST['mod']; | |||
$root = $is_mod ? $config['root'] . $config['file_mod'] . '?/' : $config['root']; | |||
if (!$is_mod) header('X-Associated-Content: "' . $root . $board['dir'] . $config['file_index'] . '"'); | |||
header('Location: ' . $root . $board['dir'] . $config['file_index'], true, $config['redirect_http']); | |||
if (!isset($_POST['json_response'])) { | |||
header('Location: ' . $root . $board['dir'] . $config['file_index'], true, $config['redirect_http']); | |||
} else { | |||
header('Content-Type: text/json'); | |||
echo json_encode(array('success' => true)); | |||
} | |||
} elseif (isset($_POST['report'])) { | |||
if (!isset($_POST['board'], $_POST['password'], $_POST['reason'])) | |||
error($config['error']['bot']); | |||
@@ -138,10 +143,13 @@ if (isset($_POST['delete'])) { | |||
$is_mod = isset($_POST['mod']) && $_POST['mod']; | |||
$root = $is_mod ? $config['root'] . $config['file_mod'] . '?/' : $config['root']; | |||
if (!$is_mod) header('X-Associated-Content: "' . $root . $board['dir'] . $config['file_index'] . '"'); | |||
header('Location: ' . $root . $board['dir'] . $config['file_index'], true, $config['redirect_http']); | |||
if (!isset($_POST['json_response'])) { | |||
header('Location: ' . $root . $board['dir'] . $config['file_index'], true, $config['redirect_http']); | |||
} else { | |||
header('Content-Type: text/json'); | |||
echo json_encode(array('success' => true)); | |||
} | |||
} elseif (isset($_POST['post'])) { | |||
if (!isset($_POST['body'], $_POST['board'])) | |||
error($config['error']['bot']); | |||
@@ -200,7 +208,7 @@ if (isset($_POST['delete'])) { | |||
} | |||
if ($post['mod'] = isset($_POST['mod']) && $_POST['mod']) { | |||
require 'inc/mod.php'; | |||
require 'inc/mod/auth.php'; | |||
if (!$mod) { | |||
// Liar. You're not a mod. | |||
error($config['error']['notamod']); | |||
@@ -275,7 +283,7 @@ if (isset($_POST['delete'])) { | |||
if ($config['allow_upload_by_url'] && isset($_POST['file_url']) && !empty($_POST['file_url'])) { | |||
$post['file_url'] = $_POST['file_url']; | |||
if (!preg_match($config['url_regex'], $post['file_url'])) | |||
if (!preg_match('@^https?://@', $post['file_url'])) | |||
error($config['error']['invalidimg']); | |||
if (mb_strpos($post['file_url'], '?') !== false) | |||
@@ -430,11 +438,6 @@ if (isset($_POST['delete'])) { | |||
wordfilters($post['body']); | |||
// Check for a flood | |||
if (!hasPermission($config['mod']['flood'], $board['uri']) && checkFlood($post)) { | |||
error($config['error']['flood']); | |||
} | |||
$post['body'] = escape_markup_modifiers($post['body']); | |||
if ($mod && isset($post['raw']) && $post['raw']) { | |||
@@ -470,10 +473,8 @@ if (isset($_POST['delete'])) { | |||
} | |||
$post['tracked_cites'] = markup($post['body'], true); | |||
require_once 'inc/filters.php'; | |||
do_filters($post); | |||
if ($post['has_file']) { | |||
if (!in_array($post['extension'], $config['allowed_ext']) && !in_array($post['extension'], $config['allowed_ext_files'])) | |||
@@ -489,9 +490,17 @@ if (isset($_POST['delete'])) { | |||
if (!is_readable($upload)) | |||
error($config['error']['nomove']); | |||
$post['filehash'] = $config['file_hash']($upload); | |||
$post['filehash'] = md5_file($upload); | |||
$post['filesize'] = filesize($upload); | |||
} | |||
if (!hasPermission($config['mod']['bypass_filters'], $board['uri'])) { | |||
require_once 'inc/filters.php'; | |||
do_filters($post); | |||
} | |||
if ($post['has_file']) { | |||
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); | |||
@@ -681,6 +690,8 @@ if (isset($_POST['delete'])) { | |||
$post['id'] = $id = post($post); | |||
insertFloodPost($post); | |||
if (isset($post['antispam_hash'])) { | |||
incrementSpamHash($post['antispam_hash']); | |||
} | |||
@@ -758,7 +769,15 @@ if (isset($_POST['delete'])) { | |||
else | |||
rebuildThemes('post', $board['uri']); | |||
header('Location: ' . $redirect, true, $config['redirect_http']); | |||
if (!isset($_POST['json_response'])) { | |||
header('Location: ' . $redirect, true, $config['redirect_http']); | |||
} else { | |||
header('Content-Type: text/json; charset=utf-8'); | |||
echo json_encode(array( | |||
'redirect' => $redirect, | |||
'id' => $id | |||
)); | |||
} | |||
} else { | |||
if (!file_exists($config['has_installed'])) { | |||
header('Location: install.php', true, $config['redirect_http']); | |||
@@ -320,6 +320,20 @@ div.pages { | |||
border-right: 1px solid #B7C5D9; | |||
border-bottom: 1px solid #B7C5D9; | |||
} | |||
div.pages.top { | |||
display: block; | |||
padding: 5px 8px; | |||
margin-bottom: 5px; | |||
position: fixed; | |||
top: 0; | |||
right: 0; | |||
opacity: 0.9; | |||
} | |||
@media screen and (max-width: 800px) { | |||
div.pages.top { | |||
display: none !important; | |||
} | |||
} | |||
div.pages a.selected { | |||
color: black; | |||
font-weight: bolder; | |||
@@ -30,7 +30,7 @@ | |||
{% endif %} | |||
<p> | |||
{% trans %}Your ban was filed on{% endtrans %} | |||
<strong>{{ ban.set|date(config.ban_date) }}</strong> {% trans %}and{% endtrans %} <span id="expires"> | |||
<strong>{{ ban.created|date(config.ban_date) }}</strong> {% trans %}and{% endtrans %} <span id="expires"> | |||
{% if ban.expires and time() >= ban.expires %} | |||
{% trans %} has since expired. Refresh the page to continue.{% endtrans %} | |||
{% elseif ban.expires %} | |||
@@ -19,8 +19,10 @@ | |||
</head> | |||
<body> | |||
{{ boardlist.top }} | |||
{% if pm %}<div class="top_notice">You have <a href="?/PM/{{ pm.id }}">an unread PM</a>{% if pm.waiting > 0 %}, plus {{ pm.waiting }} more waiting{% endif %}.</div><hr />{% endif %} | |||
{% if config.url_banner %}<img class="banner" src="{{ config.url_banner }}" {% if config.banner_width or config.banner_height %}style="{% if config.banner_width %}width:{{ config.banner_width }}px{% endif %};{% if config.banner_width %}height:{{ config.banner_height }}px{% endif %}" {% endif %}alt="" />{% endif %} | |||
<header> | |||
<h1>{{ board.url }} - {{ board.title|e }}</h1> | |||
<div class="subtitle"> | |||
@@ -44,6 +46,15 @@ | |||
{{ config.ad.top }} | |||
{% if config.page_nav_top %} | |||
<div class="pages top"> | |||
{% for page in pages %} | |||
[<a {% if page.selected %}class="selected"{% endif %}{% if not page.selected %}href="{{ page.link }}"{% endif %}>{{ page.num }}</a>]{% if loop.last %} {% endif %} | |||
{% endfor %} | |||
{{ btn.next }} | |||
</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"> | |||
@@ -52,9 +63,16 @@ | |||
{{ body }} | |||
{% include 'report_delete.html' %} | |||
</form> | |||
<div class="pages">{{ btn.prev }} {% for page in pages %} | |||
<div class="pages"> | |||
{{ btn.prev }} {% for page in pages %} | |||
[<a {% if page.selected %}class="selected"{% endif %}{% if not page.selected %}href="{{ page.link }}"{% endif %}>{{ page.num }}</a>]{% if loop.last %} {% endif %} | |||
{% endfor %} {{ btn.next }}</div> | |||
{% endfor %} {{ btn.next }} | |||
{% if config.catalog_link %} | |||
| <a href="{{ config.root }}{{ board.dir }}{{ config.catalog_link }}">Catalog</a> | |||
{% endif %} | |||
</div> | |||
{{ boardlist.bottom }} | |||
{{ config.ad.bottom }} | |||
@@ -67,6 +67,9 @@ function changeStyle(styleName, link) { | |||
if (link) { | |||
link.className = 'selected'; | |||
} | |||
if (typeof $ != 'undefined') | |||
$(window).trigger('stylesheet', styleName); | |||
} | |||
@@ -181,22 +184,28 @@ function dopost(form) { | |||
return form.elements['body'].value != "" || form.elements['file'].value != "" || (form.elements.file_url && form.elements['file_url'].value != ""); | |||
} | |||
function citeReply(id) { | |||
var body = document.getElementById('body'); | |||
function citeReply(id, with_link) { | |||
var textarea = document.getElementById('body'); | |||
if (document.selection) { | |||
// IE | |||
body.focus(); | |||
textarea.focus(); | |||
var sel = document.selection.createRange(); | |||
sel.text = '>>' + id + '\n'; | |||
} else if (body.selectionStart || body.selectionStart == '0') { | |||
// Mozilla | |||
var start = body.selectionStart; | |||
var end = body.selectionEnd; | |||
body.value = body.value.substring(0, start) + '>>' + id + '\n' + body.value.substring(end, body.value.length); | |||
} else if (textarea.selectionStart || textarea.selectionStart == '0') { | |||
var start = textarea.selectionStart; | |||
var end = textarea.selectionEnd; | |||
textarea.value = textarea.value.substring(0, start) + '>>' + id + '\n' + textarea.value.substring(end, textarea.value.length); | |||
textarea.selectionStart += ('>>' + id).length + 1; | |||
textarea.selectionEnd = textarea.selectionStart; | |||
} else { | |||
// ??? | |||
body.value += '>>' + id + '\n'; | |||
textarea.value += '>>' + id + '\n'; | |||
} | |||
if (typeof $ != 'undefined') { | |||
$(window).trigger('cite', [id, with_link]); | |||
$(textarea).change(); | |||
} | |||
} | |||
@@ -214,7 +223,7 @@ function rememberStuff() { | |||
document.forms.post.elements['email'].value = localStorage.email; | |||
if (window.location.hash.indexOf('q') == 1) | |||
citeReply(window.location.hash.substring(2)); | |||
citeReply(window.location.hash.substring(2), true); | |||
if (sessionStorage.body) { | |||
var saved = JSON.parse(sessionStorage.body); | |||
@@ -17,10 +17,10 @@ | |||
<tr{% if ban.expires != 0 and ban.expires < time() %} style="text-decoration:line-through"{% endif %}> | |||
<td style="white-space: nowrap"> | |||
<input type="checkbox" name="ban_{{ ban.id }}"> | |||
{% if ban.real_ip %} | |||
<a href="?/IP/{{ ban.ip }}">{{ ban.ip }}</a> | |||
{% if ban.single_addr %} | |||
<a href="?/IP/{{ ban.mask }}">{{ ban.mask }}</a> | |||
{% else %} | |||
{{ ban.ip|e }} | |||
{{ ban.mask }} | |||
{% endif %} | |||
</td> | |||
<td> | |||
@@ -38,15 +38,15 @@ | |||
{% endif %} | |||
</td> | |||
<td style="white-space: nowrap"> | |||
<span title="{{ ban.set|date(config.post_date) }}"> | |||
{{ ban.set|ago }} ago | |||
<span title="{{ ban.created|date(config.post_date) }}"> | |||
{{ ban.created|ago }} ago | |||
</span> | |||
</td> | |||
<td style="white-space: nowrap"> | |||
{% if ban.expires == 0 %} | |||
- | |||
{% else %} | |||
{{ (ban.expires - ban.set + time()) | until }} | |||
{{ (ban.expires - ban.created + time()) | until }} | |||
{% endif %} | |||
</td> | |||
<td style="white-space: nowrap"> | |||
@@ -77,7 +77,7 @@ | |||
{% endif %} | |||
{% endif %} | |||
{% elseif ban.mod == -1 %} | |||
{% elseif ban.creator == -1 %} | |||
<em>system</em> | |||
{% else %} | |||
<em>{% trans 'deleted?' %}</em> | |||
@@ -42,10 +42,11 @@ | |||
<input name="{{ name }}" type="text" value="{{ var.value|e }}"> | |||
{% 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> | |||
<option value="{{ constant('ADMIN') }}"{% if var.value == constant('ADMIN')%} selected{% endif %}>ADMIN</option> | |||
<option value="{{ constant('DISABLED') }}"{% if var.value == constant('DISABLED')%} selected{% endif %}>DISABLED</option> | |||
{% for group_value, group_name in config.mod.groups %} | |||
<option value="{{ group_value }}"{% if var.value == group_value %} selected{% endif %}> | |||
{{ group_name }} | |||
</option> | |||
{% endfor %} | |||
</select> | |||
{% elseif var.type == 'integer' %} | |||
<input name="{{ name }}" type="number" value="{{ var.value|e }}"> | |||
@@ -14,11 +14,14 @@ | |||
<tr> | |||
<td>{{ config.board_abbreviation|sprintf(hash.board) }}</td> | |||
<td> | |||
{% if hash.thread %} | |||
{% if hash.thread > 0 %} | |||
{{ hash.thread }} | |||
{% elseif hash.thread < 0 %} | |||
Index (page {{ - hash.thread }}) | |||
{% else %} | |||
- | |||
{% endif %}</td> | |||
{% endif %} | |||
</td> | |||
<td> | |||
<small><code>{{ hash.hash }}</code></small> | |||
</td> | |||
@@ -28,7 +31,11 @@ | |||
<td> | |||
{% if hash.expires %} | |||
<span title="{{ hash.expires|date(config.post_date) }}"> | |||
{{ hash.expires|until }} | |||
{% if hash.expires < time() %} | |||
{{ hash.expires|ago }} ago | |||
{% else %} | |||
{{ hash.expires|until }} | |||
{% endif %} | |||
</span> | |||
{% else %} | |||
- | |||
@@ -55,11 +62,14 @@ | |||
<tr> | |||
<td>{{ config.board_abbreviation|sprintf(hash.board) }}</td> | |||
<td> | |||
{% if hash.thread %} | |||
{% if hash.thread > 0 %} | |||
{{ hash.thread }} | |||
{% elseif hash.thread < 0 %} | |||
Index (page {{ - hash.thread }}) | |||
{% else %} | |||
- | |||
{% endif %}</td> | |||
{% endif %} | |||
</td> | |||
<td> | |||
<small><code>{{ hash.hash }}</code></small> | |||
</td> | |||
@@ -69,7 +79,11 @@ | |||
<td> | |||
{% if hash.expires %} | |||
<span title="{{ hash.expires|date(config.post_date) }}"> | |||
{{ hash.expires|until }} | |||
{% if hash.expires < time() %} | |||
{{ hash.expires|ago }} ago | |||
{% else %} | |||
{{ hash.expires|until }} | |||
{% endif %} | |||
</span> | |||
{% else %} | |||
- | |||
@@ -1,3 +1,40 @@ | |||
<p style="text-align:center"> | |||
Flood prevention cache: | |||
</p> | |||
<table class="modlog" style="width:1%;"> | |||
<tr> | |||
<th>#</th> | |||
<th>Time</th> | |||
<th>Board</th> | |||
<th>Post hash</th> | |||
<th>File hash</th> | |||
</tr> | |||
{% for post in flood_posts %} | |||
<tr> | |||
<td class="minimal">{{ post.id }}</td> | |||
<td class="minimal"{% if post.in_flood_table %} style="color:red" title="Still in flood prevention cache."{% endif %}> | |||
<small>{{ post.time | ago }} ago</small> | |||
</td> | |||
<td class="minimal"> | |||
<a href="?/{{ config.board_path|sprintf(post.board) }}{{ config.file_index }}"> | |||
{{ config.board_abbreviation|sprintf(post.board) }} | |||
</a> | |||
</td> | |||
<td><code>{{ post.posthash }}</code></td> | |||
<td> | |||
{% if post.filehash %} | |||
<code>{{ post.filehash }}</code> | |||
{% else %} | |||
<em>No file</em> | |||
{% endif %} | |||
</td> | |||
</tr> | |||
{% endfor %} | |||
</table> | |||
<p style="text-align:center"> | |||
Most recent {{ posts|count }} posts: | |||
</p> | |||
<table class="modlog" style="word-wrap: break-word;"> | |||
<tr> | |||
<th>Time</th> | |||
@@ -12,11 +49,13 @@ | |||
</tr> | |||
{% for post in posts %} | |||
<tr> | |||
<td class="minimal"> | |||
<td class="minimal"{% if post.in_flood_table %} style="color:red" title="Still in flood prevention cache."{% endif %}> | |||
<small>{{ post.time | ago }} ago</small> | |||
</td> | |||
<td class="minimal"> | |||
<a href="?/{{ config.board_path|sprintf(post.board) }}{{ config.file_index }}">{{ config.board_abbreviation|sprintf(post.board) }}</a> | |||
<a href="?/{{ config.board_path|sprintf(post.board) }}{{ config.file_index }}"> | |||
{{ config.board_abbreviation|sprintf(post.board) }} | |||
</a> | |||
</td> | |||
<td class="minimal" > | |||
{% if post.thread %} | |||
@@ -16,7 +16,13 @@ | |||
{% for row in result %} | |||
<tr> | |||
{% for col in row %} | |||
<td>{{ col | e }}</td> | |||
<td> | |||
{% if col != null %} | |||
{{ col | e }} | |||
{% else %} | |||
<em>NULL</em> | |||
{% endif %} | |||
</td> | |||
{% endfor %} | |||
</tr> | |||
{% endfor %} | |||
@@ -56,10 +56,10 @@ | |||
{% for ban in results %} | |||
<tr{% if ban.expires != 0 and ban.expires < time() %} style="text-decoration:line-through"{% endif %}> | |||
<td style="white-space: nowrap"> | |||
{% if ban.real_ip %} | |||
<a href="?/IP/{{ ban.ip }}#bans">{{ ban.ip }}</a> | |||
{% if ban.single_addr %} | |||
<a href="?/IP/{{ ban.mask }}#bans">{{ ban.mask }}</a> | |||
{% else %} | |||
{{ ban.ip|e }} | |||
{{ ban.mask|e }} | |||
{% endif %} | |||
</td> | |||
<td> | |||
@@ -77,15 +77,15 @@ | |||
{% endif %} | |||
</td> | |||
<td style="white-space: nowrap"> | |||
<span title="{{ ban.set|date(config.post_date) }}"> | |||
{{ ban.set|ago }} ago | |||
<span title="{{ ban.created|date(config.post_date) }}"> | |||
{{ ban.created|ago }} ago | |||
</span> | |||
</td> | |||
<td style="white-space: nowrap"> | |||
{% if ban.expires == 0 %} | |||
- | |||
{% else %} | |||
{{ (ban.expires - ban.set + time()) | until }} | |||
{{ (ban.expires - ban.created + time()) | until }} | |||
{% endif %} | |||
</td> | |||
<td style="white-space: nowrap"> | |||
@@ -116,7 +116,7 @@ | |||
{% endif %} | |||
{% endif %} | |||
{% elseif ban.mod == -1 %} | |||
{% elseif ban.creator == -1 %} | |||
<em>system</em> | |||
{% else %} | |||
<em>{% trans 'deleted?' %}</em> | |||
@@ -28,21 +28,15 @@ | |||
</tr> | |||
{% if new %} | |||
<tr> | |||
<th>{% trans 'Class' %}</th> | |||
<th>{% trans 'Group' %}</th> | |||
<td> | |||
<ul style="padding:5px 8px;list-style:none"> | |||
<li> | |||
<input type="radio" name="type" id="janitor" value="{{ constant('JANITOR') }}"> | |||
<label for="janitor">{% trans 'Janitor' %}</label> | |||
</li> | |||
<li> | |||
<input type="radio" name="type" id="mod" value="{{ constant('MOD') }}" checked> | |||
<label for="mod">{% trans 'Mod' %}</label> | |||
</li> | |||
<li> | |||
<input type="radio" name="type" id="admin" value="{{ constant('Admin') }}"> | |||
<label for="admin">{% trans 'Admin' %}</label> | |||
</li> | |||
{% for group_value, group_name in config.mod.groups if group_name != 'Disabled' %} | |||
<li> | |||
<input type="radio" name="type" id="group_{{ group_name }}" value="{{ group_value }}"> | |||
<label for="group_{{ group_name }}">{% trans group_name %}</label> | |||
</li> | |||
{% endfor %} | |||
</ul> | |||
</td> | |||
</tr> | |||
@@ -15,9 +15,10 @@ | |||
<td><small>{{ user.id }}</small></td> | |||
<td>{{ user.username|e }}</td> | |||
<td> | |||
{% if user.type == constant('JANITOR') %}{% trans 'Janitor' %} | |||
{% elseif user.type == constant('MOD') %}{% trans 'Mod' %} | |||
{% elseif user.type == constant('ADMIN') %}{% trans 'Admin' %} | |||
{% if config.mod.groups[user.type] %} | |||
{{ config.mod.groups[user.type] }} | |||
{% else %} | |||
<em>{% trans 'Unknown' %}</em> ({{ user.type }}) | |||
{% endif %} | |||
</td> | |||
<td> | |||
@@ -46,11 +47,11 @@ | |||
</td> | |||
{% endif %} | |||
<td> | |||
{% if mod|hasPermission(config.mod.promoteusers) and user.type < constant('ADMIN') %} | |||
{% if mod|hasPermission(config.mod.promoteusers) and user.type < constant(config.mod.groups[0:-1]|last) %} | |||
<a style="float:left;text-decoration:none" href="?/users/{{ user.id }}/promote" title="{% trans 'Promote' %}">▲</a> | |||
{% endif %} | |||
{% if mod|hasPermission(config.mod.promoteusers) and user.type > constant('JANITOR') %} | |||
<a style="float:left;text-decoration:none" href="?/users/{{ user.id }}/demote" title="{% trans 'Demote' %}">▼</a> | |||
{% if mod|hasPermission(config.mod.promoteusers) and user.type > constant(config.mod.groups|first) %} | |||
<a style="float:left;text-decoration:none" href="?/users/{{ user.id }}/demote" title="{% trans 'Demote' %}"{% if mod.id == user.id %} onclick="return confirm('{% trans 'Are you sure you want to demote yourself?' %}')"{% endif %}>▼</a> | |||
{% endif %} | |||
{% if mod|hasPermission(config.mod.modlog) %} | |||
<a class="unimportant" style="margin-left:5px;float:right" href="?/log:{{ user.username|e }}">[{% trans 'log' %}]</a> | |||
@@ -100,7 +100,7 @@ | |||
</tr> | |||
<tr> | |||
<th>{% trans 'IP' %}</th> | |||
<td>{{ ban.ip }}</td> | |||
<td>{{ ban.mask }}</td> | |||
</tr> | |||
<tr> | |||
<th>{% trans 'Reason' %}</th> | |||
@@ -124,7 +124,7 @@ | |||
</tr> | |||
<tr> | |||
<th>{% trans 'Set' %}</th> | |||
<td>{{ ban.set|date(config.post_date) }}</td> | |||
<td>{{ ban.created|date(config.post_date) }}</td> | |||
</tr> | |||
<tr> | |||
<th>{% trans 'Expires' %}</th> | |||