diff --git a/inc/bans.php b/inc/bans.php
new file mode 100644
index 00000000..652d42cc
--- /dev/null
+++ b/inc/bans.php
@@ -0,0 +1,258 @@
+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 ? "$mask" : $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 ? "$mask" : $mask) .
+ ' (#' . $pdo->lastInsertId() . ')' .
+ ' with ' . ($reason ? 'reason: ' . utf8tohtml($reason) . '' : 'no reason'));
+ }
+ return $pdo->lastInsertId();
+ }
+}