diff --git a/inc/config.php b/inc/config.php index 84de9cfb..2bdd3131 100644 --- a/inc/config.php +++ b/inc/config.php @@ -523,9 +523,31 @@ // pure-PHP geolocation library. $config['country_flags'] = false; +/* +* ==================== +* Ban settings +* ==================== +*/ + // Require users to see the ban page at least once for a ban even if it has since expired. $config['require_ban_view'] = true; + // Show the post the user was banned for on the "You are banned" page. + $config['ban_show_post'] = false; + + // Optional HTML to append to "You are banned" pages. For example, you could include instructions and/or + // a link to an email address or IRC chat room to appeal the ban. + $config['ban_page_extra'] = ''; + + // Allow users to appeal bans through Tinyboard. + $config['ban_appeals'] = false; + + // Do not allow users to appeal bans that are shorter than this length (in seconds). + $config['ban_appeals_min_length'] = 60 * 60 * 6; // 6 hours + + // How many ban appeals can be made for a single ban? + $config['ban_appeals_max'] = 1; + /* * ==================== * Markup settings @@ -821,13 +843,6 @@ // Automatically remove unnecessary whitespace when compiling HTML files from templates. $config['minify_html'] = true; - // Show the post the user was banned for on the "You are banned" page. - $config['ban_show_post'] = false; - - // Optional HTML to append to "You are banned" pages. For example, you could include instructions and/or - // a link to an email address or IRC chat room to appeal the ban. - $config['ban_page_extra'] = ''; - // Display flags (when available). This config option has no effect unless poster flags are enabled (see // $config['country_flags']). Disable this if you want all previously-assigned flags to be hidden. $config['display_flags'] = true; @@ -1322,6 +1337,10 @@ $config['mod']['debug_sql'] = DISABLED; // Edit the current configuration (via web interface) $config['mod']['edit_config'] = ADMIN; + // View ban appeals + $config['mod']['view_ban_appeals'] = MOD; + // Accept and deny ban appeals + $config['mod']['ban_appeals'] = MOD; // Config editor permissions $config['mod']['config'] = array(); diff --git a/inc/functions.php b/inc/functions.php index cfde4640..078911ea 100644 --- a/inc/functions.php +++ b/inc/functions.php @@ -625,11 +625,16 @@ function displayBan($ban) { $ban['ip'] = $_SERVER['REMOTE_ADDR']; if ($ban['post'] && isset($ban['post']['board'], $ban['post']['id'])) { - openBoard($ban['post']['board']); - - $query = query(sprintf("SELECT `thumb`, `file` FROM ``posts_%s`` WHERE `id` = " . (int)$ban['post']['id'], $board['uri'])); - if ($_post = $query->fetch(PDO::FETCH_ASSOC)) { - $ban['post'] = array_merge($ban['post'], $_post); + if (openBoard($ban['post']['board'])) { + + $query = query(sprintf("SELECT `thumb`, `file` FROM ``posts_%s`` WHERE `id` = " . + (int)$ban['post']['id'], $board['uri'])); + if ($_post = $query->fetch(PDO::FETCH_ASSOC)) { + $ban['post'] = array_merge($ban['post'], $_post); + } else { + $ban['post']['file'] = 'deleted'; + $ban['post']['thumb'] = false; + } } else { $ban['post']['file'] = 'deleted'; $ban['post']['thumb'] = false; @@ -641,6 +646,21 @@ function displayBan($ban) { $post = new Thread($ban['post'], null, false, false); } } + + $denied_appeals = array(); + $pending_appeal = false; + + if ($config['ban_appeals']) { + $query = query("SELECT `time`, `denied` FROM `ban_appeals` WHERE `ban_id` = " . (int)$ban['id']) or error(db_error()); + while ($ban_appeal = $query->fetch(PDO::FETCH_ASSOC)) { + if ($ban_appeal['denied']) { + $denied_appeals[] = $ban_appeal['time']; + } else { + $pending_appeal = $ban_appeal['time']; + } + } + } + // Show banned page and exit die( Element('page.html', array( @@ -651,7 +671,9 @@ function displayBan($ban) { 'config' => $config, 'ban' => $ban, 'board' => $board, - 'post' => isset($post) ? $post->build(true) : false + 'post' => isset($post) ? $post->build(true) : false, + 'denied_appeals' => $denied_appeals, + 'pending_appeal' => $pending_appeal ) )) )); diff --git a/inc/mod/pages.php b/inc/mod/pages.php index 525a3d16..bae00a0f 100644 --- a/inc/mod/pages.php +++ b/inc/mod/pages.php @@ -197,37 +197,7 @@ function mod_search($type, $search_query_escaped, $page_no = 1) { // Form a series of LIKE clauses for the query. // This gets a little complicated. - // Escape "escape" character - $query = str_replace('!', '!!', $query); - - // Escape SQL wildcard - $query = str_replace('%', '!%', $query); - - // Use asterisk as wildcard instead - $query = str_replace('*', '%', $query); - - $query = str_replace('`', '!`', $query); - - // Array of phrases to match - $match = array(); - - // Exact phrases ("like this") - if (preg_match_all('/"(.+?)"/', $query, $exact_phrases)) { - $exact_phrases = $exact_phrases[1]; - foreach ($exact_phrases as $phrase) { - $query = str_replace("\"{$phrase}\"", '', $query); - $match[] = $pdo->quote($phrase); - } - } - - // Non-exact phrases (ie. plain keywords) - $keywords = explode(' ', $query); - foreach ($keywords as $word) { - if (empty($word)) - continue; - $match[] = $pdo->quote($word); - } - + // Which `field` to search? if ($type == 'posts') $sql_field = array('body_nomarkup', 'filename', 'subject', 'filehash', 'ip', 'name', 'trip'); @@ -238,22 +208,6 @@ function mod_search($type, $search_query_escaped, $page_no = 1) { if ($type == 'log') $sql_field = 'text'; - // Build the "LIKE 'this' AND LIKE 'that'" etc. part of the SQL query - $sql_like = ''; - foreach ($match as $phrase) { - if (!empty($sql_like)) - $sql_like .= ' AND '; - $phrase = preg_replace('/^\'(.+)\'$/', '\'%$1%\'', $phrase); - if (is_array($sql_field)) { - foreach ($sql_field as $field) { - $sql_like .= '`' . $field . '` LIKE ' . $phrase . ' ESCAPE \'!\' OR'; - } - $sql_like = preg_replace('/ OR$/', '', $sql_like); - } else { - $sql_like .= '`' . $sql_field . '` LIKE ' . $phrase . ' ESCAPE \'!\''; - } - } - // Compile SQL query @@ -884,6 +838,68 @@ function mod_bans($page_no = 1) { mod_page(_('Ban list'), 'mod/ban_list.html', array('bans' => $bans, 'count' => Bans::count())); } +function mod_ban_appeals() { + global $config, $board; + + if (!hasPermission($config['mod']['view_ban_appeals'])) + error($config['error']['noaccess']); + + // Remove stale ban appeals + query("DELETE FROM ``ban_appeals`` WHERE NOT EXISTS (SELECT 1 FROM ``bans`` WHERE `ban_id` = ``bans``.`id`)") + or error(db_error()); + + if (isset($_POST['appeal_id']) && (isset($_POST['unban']) || isset($_POST['deny']))) { + if (!hasPermission($config['mod']['ban_appeals'])) + error($config['error']['noaccess']); + if (isset($_POST['unban'])) { + $query = query("SELECT `ban_id` FROM ``ban_appeals`` WHERE `id` = " . + (int)$_POST['appeal_id']) or error(db_error()); + if ($ban_id = $query->fetchColumn()) { + Bans::delete($ban_id); + query("DELETE FROM ``ban_appeals`` WHERE `id` = " . (int)$_POST['appeal_id']) or error(db_error()); + } + } else { + query("UPDATE ``ban_appeals`` SET `denied` = 1 WHERE `id` = " . (int)$_POST['appeal_id']) or error(db_error()); + } + + header('Location: ?/ban-appeals', true, $config['redirect_http']); + return; + } + + $query = query("SELECT *, ``ban_appeals``.`id` AS `id` FROM ``ban_appeals`` + LEFT JOIN ``bans`` ON `ban_id` = ``bans``.`id` + WHERE `denied` != 1 ORDER BY `time`") or error(db_error()); + $ban_appeals = $query->fetchAll(PDO::FETCH_ASSOC); + foreach ($ban_appeals as &$ban) { + if ($ban['post']) + $ban['post'] = json_decode($ban['post'], true); + $ban['mask'] = Bans::range_to_string(array($ban['ipstart'], $ban['ipend'])); + + if ($ban['post'] && isset($ban['post']['board'], $ban['post']['id'])) { + if (openBoard($ban['post']['board'])) { + $query = query(sprintf("SELECT `thumb`, `file` FROM ``posts_%s`` WHERE `id` = " . + (int)$ban['post']['id'], $board['uri'])); + if ($_post = $query->fetch(PDO::FETCH_ASSOC)) { + $ban['post'] = array_merge($ban['post'], $_post); + } else { + $ban['post']['file'] = 'deleted'; + $ban['post']['thumb'] = false; + } + } else { + $ban['post']['file'] = 'deleted'; + $ban['post']['thumb'] = false; + } + + if ($ban['post']['thread']) { + $ban['post'] = new Post($ban['post']); + } else { + $ban['post'] = new Thread($ban['post'], null, false, false); + } + } + } + + mod_page(_('Ban appeals'), 'mod/ban_appeals.html', array('ban_appeals' => $ban_appeals)); +} function mod_lock($board, $unlock, $post) { global $config; diff --git a/mod.php b/mod.php index 6ef4644d..ca6d971e 100644 --- a/mod.php +++ b/mod.php @@ -59,6 +59,7 @@ $pages = array( '/IP/([\w.:]+)/remove_note/(\d+)' => 'ip_remove_note', // remove note from ip address '/bans' => 'bans', // ban list '/bans/(\d+)' => 'bans', // ban list + '/ban-appeals' => 'ban_appeals', // view ban appeals '/search' => 'search_redirect', // search '/search/(posts|IP_notes|bans|log)/(.+)/(\d+)' => 'search', // search diff --git a/post.php b/post.php index 1328bed3..b1528870 100644 --- a/post.php +++ b/post.php @@ -763,6 +763,47 @@ if (isset($_POST['delete'])) { 'id' => $id )); } +} elseif (isset($_POST['appeal'])) { + if (!isset($_POST['ban_id'])) + error($config['error']['bot']); + + $ban_id = (int)$_POST['ban_id']; + + $bans = Bans::find($_SERVER['REMOTE_ADDR']); + foreach ($bans as $_ban) { + if ($_ban['id'] == $ban_id) { + $ban = $_ban; + break; + } + } + + if (!isset($ban)) { + error(_("That ban doesn't exist or is not for you.")); + } + + if ($ban['expires'] && $ban['expires'] - $ban['created'] <= $config['ban_appeals_min_length']) { + error(_("You cannot appeal a ban of this length.")); + } + + $query = query("SELECT `denied` FROM ``ban_appeals`` WHERE `ban_id` = $ban_id") or error(db_error()); + $ban_appeals = $query->fetchAll(PDO::FETCH_COLUMN); + + if (count($ban_appeals) >= $config['ban_appeals_max']) { + error(_("You cannot appeal this ban again.")); + } + + foreach ($ban_appeals as $is_denied) { + if (!$is_denied) + error(_("There is already a pending appeal for this ban.")); + } + + $query = prepare("INSERT INTO ``ban_appeals`` VALUES (NULL, :ban_id, :time, :message, 0)"); + $query->bindValue(':ban_id', $ban_id, PDO::PARAM_INT); + $query->bindValue(':time', time(), PDO::PARAM_INT); + $query->bindValue(':message', $_POST['appeal']); + $query->execute() or error(db_error($query)); + + displayBan($ban); } else { if (!file_exists($config['has_installed'])) { header('Location: install.php', true, $config['redirect_http']); diff --git a/stylesheets/style.css b/stylesheets/style.css index ef12a5f5..31b866ef 100644 --- a/stylesheets/style.css +++ b/stylesheets/style.css @@ -424,4 +424,10 @@ table.mod.config-editor input[type="text"] { p.intro.thread-hidden { margin: 0px; padding: 0px; +} +form.ban-appeal { + margin: 9px 20px; +} +form.ban-appeal textarea { + display: block; } \ No newline at end of file diff --git a/templates/banned.html b/templates/banned.html index 79648156..666521a4 100644 --- a/templates/banned.html +++ b/templates/banned.html @@ -77,16 +77,60 @@

{% trans %}Your IP address is{% endtrans %} {{ ban.ip }}.

- {% if post %} + {% if config.ban_page_extra %} +

{{ config.ban_page_extra }}

+ {% endif %} + + {% if post and config.ban_show_post %}
-

You were banned for the following post on {{ board.url }}:

+

{% trans %}You were banned for the following post on {% endtrans %}{{ board.url }}:

{{ post }}
{% endif %} - {% if config.ban_page_extra %} -

{{ config.ban_page_extra }}

+ {% if config.ban_appeals %} +
+ {% if pending_appeal %} +

+ {% trans %}You submitted an appeal for this ban on{% endtrans %} + {{ pending_appeal|date(config.ban_date) }}. {% trans %}It is still pending{% endtrans %}. +

+ {% elseif denied_appeals|length >= config.ban_appeals_max %} + {% if denied_appeals|length == 1 %} +

+ {% trans %}You appealed this ban on{% endtrans %} + {{ denied_appeals[0]|date(config.ban_date) }} + {% trans %}and it was denied. You may not appeal this ban again.{% endtrans %} +

+ {% else %} +

{% trans %}You have submitted the maximum number of ban appeals allowed. You may not appeal this ban again.{% endtrans %}

+ {% endif %} + {% else %} + {% if denied_appeals|length %} + {% if denied_appeals|length == 1 %} +

+ {% trans %}You appealed this ban on{% endtrans %} + {{ denied_appeals[0]|date(config.ban_date) }} + {% trans %}and it was denied.{% endtrans %} +

+

{% trans %}You may appeal this ban again. Please enter your reasoning below.{% endtrans %}

+ {% else %} +

+ {% trans %}You last appealed this ban on{% endtrans %} + {{ denied_appeals[denied_appeals|length - 1]|date(config.ban_date) }} + {% trans %}and it was denied.{% endtrans %} +

+

{% trans %}You may appeal this ban again. Please enter your reasoning below.{% endtrans %}

+ {% endif %} + {% else %} +

{% trans %}You may appeal this ban. Please enter your reasoning below.{% endtrans %}

+ {% endif %} +
+ + + +
+ {% endif %} {% endif %} -{% endfilter %} - +{% endfilter %} \ No newline at end of file diff --git a/templates/mod/ban_appeals.html b/templates/mod/ban_appeals.html new file mode 100644 index 00000000..f43b7db5 --- /dev/null +++ b/templates/mod/ban_appeals.html @@ -0,0 +1,106 @@ +{% for ban in ban_appeals %} + +
+ + + + + + {% if mod|hasPermission(config.mod.show_ip, board.uri) %} + + + + + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans 'Status' %} + {% if config.mod.view_banexpired and ban.expires != 0 and ban.expires < time() %} + {% trans 'Expired' %} + {% else %} + {% trans 'Active' %} + {% endif %} +
{% trans 'IP' %}{{ ban.mask }}
{% trans 'Reason' %} + {% if ban.reason %} + {{ ban.reason }} + {% else %} + {% trans 'no reason' %} + {% endif %} +
{% trans 'Board' %} + {% if ban.board %} + {{ config.board_abbreviation|sprintf(ban.board) }} + {% else %} + {% trans 'all boards' %} + {% endif %} +
{% trans 'Set' %}{{ ban.created|date(config.post_date) }}
{% trans 'Expires' %} + {% if ban.expires %} + {{ ban.expires|date(config.post_date) }} + {% else %} + {% trans 'never' %} + {% endif %} +
{% trans 'Seen' %} + {% if ban.seen %} + {% trans 'Yes' %} + {% else %} + {% trans 'No' %} + {% endif %} +
{% trans 'Staff' %} + {% if ban.username %} + {{ ban.username|e }} + {% else %} + {% trans 'deleted?' %} + {% endif %} +
+ + + + + + + + + + + {% if mod|hasPermission(config.mod.ban_appeals, board.uri) %} + + + + + {% endif %} +
{% trans 'Appeal time' %}{{ ban.time|date(config.post_date) }}
{% trans 'Appeal reason' %}{{ ban.message|e }}
{% trans 'Action' %} + + + +
+ + {% if ban.post %} +
+ {{ ban.post.build(true) }} +
+ {% endif %} +
+
+ +{% endfor %} \ No newline at end of file diff --git a/templates/mod/dashboard.html b/templates/mod/dashboard.html index e2f7507d..916f0c03 100644 --- a/templates/mod/dashboard.html +++ b/templates/mod/dashboard.html @@ -86,6 +86,9 @@ {% if mod|hasPermission(config.mod.view_banlist) %}
  • {% trans 'Ban list' %}
  • {% endif %} + {% if config.ban_appeals and mod|hasPermission(config.mod.view_ban_appeals) %} +
  • {% trans 'Ban appeals' %}
  • + {% endif %} {% if mod|hasPermission(config.mod.manageusers) %}
  • {% trans 'Manage users' %}
  • {% elseif mod|hasPermission(config.mod.change_password) %}