@@ -1015,7 +1015,7 @@ | |||
$config['mod']['master_pm'] = ADMIN; | |||
// Rebuild everything | |||
$config['mod']['rebuild'] = ADMIN; | |||
// Search through posts | |||
// Search through posts, IP address notes and bans | |||
$config['mod']['search'] = JANITOR; | |||
// Read the moderator noticeboard | |||
$config['mod']['noticeboard'] = JANITOR; | |||
@@ -162,6 +162,112 @@ function mod_dashboard() { | |||
mod_page(_('Dashboard'), 'mod/dashboard.html', $args); | |||
} | |||
function mod_search_redirect() { | |||
global $config; | |||
if (!hasPermission($config['mod']['search'])) | |||
error($config['error']['noaccess']); | |||
if (isset($_POST['query'], $_POST['type']) && in_array($_POST['type'], array('posts', 'IP_notes', 'bans'))) { | |||
$query = $_POST['query']; | |||
$query = urlencode($query); | |||
$query = str_replace('_', '%5F', $query); | |||
$query = str_replace('+', '_', $query); | |||
header('Location: ?/search/' . $_POST['type'] . '/' . $query, true, $config['redirect_http']); | |||
} else { | |||
header('Location: ?/', true, $config['redirect_http']); | |||
} | |||
} | |||
function mod_search($type, $query) { | |||
global $pdo, $config; | |||
if (!hasPermission($config['mod']['search'])) | |||
error($config['error']['noaccess']); | |||
// Unescape query | |||
$query = str_replace('_', ' ', $query); | |||
$query = urldecode($query); | |||
$search_query = $query; | |||
// 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); | |||
// Array of phrases to match | |||
$match = array(); | |||
// Exact phrases ("like this") | |||
if (preg_match_all('/"(.+?)"/', $query, $matches)) { | |||
foreach ($matches[1] 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 = 'body'; | |||
if ($type == 'IP_notes') | |||
$sql_field = 'body'; | |||
if ($type == 'bans') | |||
$sql_field = 'reason'; | |||
// 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); | |||
$sql_like .= '`' . $sql_field . '` LIKE ' . $phrase . ' ESCAPE \'!\''; | |||
} | |||
if ($type == 'posts') { | |||
error('Searching posts is under development. Sorry.'); | |||
} | |||
if ($type == 'IP_notes') { | |||
$query = query('SELECT * FROM `ip_notes` LEFT JOIN `mods` ON `mod` = `mods`.`id` WHERE ' . $sql_like . ' ORDER BY `time` DESC') or error(db_error()); | |||
$results = $query->fetchAll(PDO::FETCH_ASSOC); | |||
} | |||
if ($type == 'bans') { | |||
$query = 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`') or error(db_error()); | |||
$results = $query->fetchAll(PDO::FETCH_ASSOC); | |||
foreach ($results as &$ban) { | |||
if (filter_var($ban['ip'], FILTER_VALIDATE_IP) !== false) | |||
$ban['real_ip'] = true; | |||
} | |||
} | |||
// $results now contains the search results | |||
mod_page(_('Search results'), 'mod/search_results.html', array( | |||
'search_type' => $type, | |||
'search_query' => $search_query, | |||
'result_count' => count($results), | |||
'results' => $results | |||
)); | |||
} | |||
function mod_edit_board($boardName) { | |||
global $board, $config; | |||
@@ -56,6 +56,9 @@ $pages = array( | |||
'/IP/([\w.:]+)/remove_note/(\d+)' => 'ip_remove_note', // remove note from ip address | |||
'/bans' => 'bans', // ban list | |||
'/bans/(\d+)' => 'bans', // ban list | |||
'/search' => 'search_redirect', // search | |||
'/search/(posts|IP_notes|bans)/(.+)' => 'search', // search | |||
// CSRF-protected moderator actions | |||
'/ban' => 'secure_POST ban', // new ban | |||
@@ -101,22 +101,17 @@ | |||
</ul> | |||
</fieldset> | |||
{# | |||
{% if mod|hasPermission(config.mod.search) %} | |||
<fieldset> | |||
<legend>{% trans 'Search' %}</legend> | |||
<ul> | |||
<li> | |||
<form style="display:inline" action="?/search" method="post"> | |||
<label style="display:inline" for="search">{% trans 'Phrase:' %}</label> | |||
<input id="search" name="search" type="text" size="35"> | |||
<input type="submit" value="{% trans 'Search' %}"> | |||
</form> | |||
<p class="unimportant">{% trans '(Search is case-insensitive, and based on keywords. To match exact phrases, use "quotes". Use an asterisk (*) for wildcard.)' %}</p> | |||
{% include 'mod/search_form.html' %} | |||
</li> | |||
</ul> | |||
</fieldset> | |||
#} | |||
{% endif %} | |||
{% if config.debug %} | |||
<fieldset> | |||
@@ -0,0 +1,15 @@ | |||
<form style="display:inline" action="?/search" method="post"> | |||
<label style="display:inline" for="search">{% trans 'Phrase:' %}</label> | |||
<input id="search" name="query" type="text" size="60" value="{{ search_query|e }}"> | |||
<select name="type"> | |||
<option value="posts"{% if search_type == 'posts'%} selected{% endif %}>Posts</option> | |||
{% if mod|hasPermission(config.mod.view_notes) and mod|hasPermission(config.mod.show_ip) %} | |||
<option value="IP_notes"{% if search_type == 'IP_notes'%} selected{% endif %}>IP address notes</option> | |||
{% endif %} | |||
{% if mod|hasPermission(config.mod.view_banlist) %} | |||
<option value="bans"{% if search_type == 'bans'%} selected{% endif %}>Bans</option> | |||
{% endif %} | |||
</select> | |||
<input type="submit" value="{% trans 'Search' %}"> | |||
</form> | |||
<p class="unimportant">{% trans '(Search is case-insensitive and based on keywords. To match exact phrases, use "quotes". Use an asterisk (*) for wildcard.)' %}</p> |
@@ -0,0 +1,129 @@ | |||
<fieldset style="margin-bottom:20px"> | |||
<legend>{% trans 'Search' %}</legend> | |||
<ul> | |||
<li> | |||
{% include 'mod/search_form.html' %} | |||
</li> | |||
</ul> | |||
</fieldset> | |||
<p style="text-align:center">Showing {{ result_count }} result{% if result_count != 1 %}s{% endif %}.</p> | |||
{% if search_type == 'IP_notes' %} | |||
<table class="modlog"> | |||
<tr> | |||
<th>{% trans 'IP address' %}</th> | |||
<th>{% trans 'Staff' %}</th> | |||
<th>{% trans 'Note' %}</th> | |||
<th>{% trans 'Date' %}</th> | |||
</tr> | |||
{% for note in results %} | |||
<tr> | |||
<td class="minimal"> | |||
<a href="?/IP/{{ note.ip }}#notes">{{ note.ip }}</a> | |||
</td> | |||
<td class="minimal"> | |||
{% if note.username %} | |||
<a href="?/new_PM/{{ note.username|e }}">{{ note.username|e }}</a> | |||
{% else %} | |||
<em>{% trans 'deleted?' %}</em> | |||
{% endif %} | |||
</td> | |||
<td> | |||
{{ note.body }} | |||
</td> | |||
<td class="minimal"> | |||
{{ note.time|date(config.post_date) }} | |||
</td> | |||
</tr> | |||
{% endfor %} | |||
</table> | |||
{% endif %} | |||
{% if search_type == 'bans' %} | |||
<table class="modlog" style="width:100%"> | |||
<tr> | |||
<th>{% trans 'IP address/mask' %}</th> | |||
<th>{% trans 'Reason' %}</th> | |||
<th>{% trans 'Board' %}</th> | |||
<th>{% trans 'Set' %}</th> | |||
<th>{% trans 'Duration' %}</th> | |||
<th>{% trans 'Expires' %}</th> | |||
<th>{% trans 'Seen' %}</th> | |||
<th>{% trans 'Staff' %}</th> | |||
</tr> | |||
{% 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> | |||
{% else %} | |||
{{ ban.ip|e }} | |||
{% endif %} | |||
</td> | |||
<td> | |||
{% if ban.reason %} | |||
{{ ban.reason }} | |||
{% else %} | |||
- | |||
{% endif %} | |||
</td> | |||
<td style="white-space: nowrap"> | |||
{% if ban.board %} | |||
{{ config.board_abbreviation|sprintf(ban.board) }} | |||
{% else %} | |||
<em>{% trans 'all boards' %}</em> | |||
{% endif %} | |||
</td> | |||
<td style="white-space: nowrap"> | |||
<span title="{{ ban.set|date(config.post_date) }}"> | |||
{{ ban.set|ago }} ago | |||
</span> | |||
</td> | |||
<td style="white-space: nowrap"> | |||
{% if ban.expires == 0 %} | |||
- | |||
{% else %} | |||
{{ (ban.expires - ban.set + time()) | until }} | |||
{% endif %} | |||
</td> | |||
<td style="white-space: nowrap"> | |||
{% if ban.expires == 0 %} | |||
<em>{% trans 'never' %}</em> | |||
{% else %} | |||
{{ ban.expires|date(config.post_date) }} | |||
{% if ban.expires > time() %} | |||
<small>(in {{ ban.expires|until }})</small> | |||
{% endif %} | |||
{% endif %} | |||
</td> | |||
<td> | |||
{% if ban.seen %} | |||
{% trans 'Yes' %} | |||
{% else %} | |||
{% trans 'No' %} | |||
{% endif %} | |||
</td> | |||
<td> | |||
{% if ban.username %} | |||
{% if mod|hasPermission(config.mod.view_banstaff) %} | |||
<a href="?/new_PM/{{ ban.username|e }}">{{ ban.username|e }}</a> | |||
{% else %} | |||
{% if mod|hasPermission(config.mod.view_banquestionmark) %} | |||
<em>?</em> | |||
{% else %} | |||
{% endif %} | |||
{% endif %} | |||
{% elseif ban.mod == -1 %} | |||
<em>system</em> | |||
{% else %} | |||
<em>{% trans 'deleted?' %}</em> | |||
{% endif %} | |||
</td> | |||
</tr> | |||
{% endfor %} | |||
</table> | |||
{% endif %} |