Browse Source

Added Feature - Shadow Delete of Posts and Threads (can be restored if deleted by mod) - Still need a way to see and restore deleted threads

main
PupperWoff 7 years ago
committed by discomrade
parent
commit
c353e1284d
  1. 83
      UPDATE_SCRIPT__SHADOW_DELETE.php
  2. 1
      composer.json
  3. 21
      inc/config.php
  4. 62
      inc/functions.php
  5. 119
      inc/mod/pages.php
  6. 418
      inc/shadow-delete.php
  7. 7
      mod.php
  8. 2
      post.php
  9. 6
      stylesheets/style.css
  10. 2
      templates/post/file_controls.html
  11. 4
      templates/post/fileinfo.html
  12. 8
      templates/post/image.html
  13. 125
      templates/post/post_controls.html
  14. 2
      templates/post_reply.html

83
UPDATE_SCRIPT__SHADOW_DELETE.php

@ -0,0 +1,83 @@
<?php
// require 'inc/config.php';
// require 'inc/config_instance.php';
require 'inc/functions.php';
global $config;
// Check so only ADMIN can run script
check_login(true);
if (!$mod || $mod['type'] != ADMIN)
die("You need to be logged in as admin");
$page['title'] = 'Updating Database - Temporarly Deletion of Threads';
$step = isset($_GET['step']) ? round($_GET['step']) : 0;
switch($step)
{
default:
case 0:
$page['body'] = '<p style="text-align:center">You are about to update the database to allow temporarely deletion of threads (with thimeout for permanent deletion).</p>';
$page['body'] .= '<p style="text-align:center"><a href="?step=2">Click here to update database entries and folder structure.</a></p>';
break;
case 2:
$page['body'] = '<p style="text-align:center">Database have been updated.</p>';
$sql_errors = "";
$file_errors = "";
// Update posts_* table to archive function
// Get list of boards
$boards = listBoards();
foreach ($boards as &$_board) {
// Create Temp Posts Table for Boards
$query = Element('../UPDATE_SQL__SHADOW_DELETE.sql');
if (mysql_version() < 50503)
$query = preg_replace('/(CHARSET=|CHARACTER SET )utf8mb4/', '$1utf8', $query);
query($query) or $sql_errors .= "<li>Add Shared Shadow Post DB Tables<br/>" . db_error() . '</li>';
// Create Temp Posts Table for Boards
$query = Element('posts.sql', array('board' => $_board['uri']));
$query = str_replace("``posts_", "``shadow_posts_", $query);
if (mysql_version() < 50503)
$query = preg_replace('/(CHARSET=|CHARACTER SET )utf8mb4/', '$1utf8', $query);
query($query) or $sql_errors .= sprintf("<li>Add Shadow Post DB for %s<br/>", $_board['uri']) . db_error() . '</li>';
$_board['dir'] = sprintf($config['board_path'], $_board['uri']);
// Create TEMP Folders to save files in
if (!file_exists($_board['dir'] . $config['dir']['shadow_del']))
@mkdir($_board['dir'] . $config['dir']['shadow_del'], 0777)
or $file_errors .= "Couldn't create " . $_board['dir'] . $config['dir']['shadow_del'] . ". Check permissions.<br/>";
if (!file_exists($_board['dir'] . $config['dir']['shadow_del'] . $config['dir']['img']))
@mkdir($_board['dir'] . $config['dir']['shadow_del'] . $config['dir']['img'], 0777)
or $file_errors .= "Couldn't create " . $_board['dir'] . $config['dir']['shadow_del'] . $config['dir']['img'] . ". Check permissions.<br/>";
if (!file_exists($_board['dir'] . $config['dir']['shadow_del'] . $config['dir']['thumb']))
@mkdir($_board['dir'] . $config['dir']['shadow_del'] . $config['dir']['thumb'], 0777)
or $file_errors .= "Couldn't create " . $_board['dir'] . $config['dir']['shadow_del'] . $config['dir']['thumb'] . ". Check permissions.<br/>";
}
if (!empty($sql_errors))
$page['body'] .= '<div class="ban"><h2>SQL errors</h2><p>SQL errors were encountered when trying to update the database.</p><p>The errors encountered were:</p><ul>' . $sql_errors . '</ul></div>';
if (!empty($file_errors))
$page['body'] .= '<div class="ban"><h2>File System errors</h2><p>File System errors were encountered when trying to create folders.</p><p>The errors encountered were:</p><ul>' . $file_errors . '</ul></div>';
break;
}
echo Element('page.html', $page);
?>
<!-- There is probably a much better way to do this, but eh. -->
<link rel="stylesheet" type="text/css" href="stylesheets/style.css" />

1
composer.json

@ -28,6 +28,7 @@
"inc/polyfill.php",
"inc/announcements.php",
"inc/archive.php",
"inc/shadow-delete.php",
"inc/functions.php"
]
},

21
inc/config.php

@ -1304,6 +1304,18 @@
$config['dir']['res'] = 'res/';
// Shadow Del dir for files (non perm deleted)
$config['dir']['shadow_del'] = 'tempura/';
// Use shadow delete instead of immediate permanent delete
$config['shadow_del']['use'] = true;
// Hash Seed used to obscure filenames of shadow deleted files for posts
$config['shadow_del']['filename_seed'] = '5azs5co3wAN67tlqbINEmWuERtTX4FatsMVe446JbHVIJbZyjephDsdRtULw501';
// Lifetime for shadow deleted threads before permanent delete (ex. "60 minutes", "6 hours", "1 day", "1 week")
$config['shadow_del']['lifetime'] = "6 hours";
// Directory for archived threads
$config['dir']['archive'] = 'archive/';
// Directory for "Featured Threads" (threads makred for permanent storage)
@ -1485,6 +1497,9 @@
$config['mod']['link_cycle'] = '[Cycle]';
$config['mod']['link_uncycle'] = '[-Cycle]';
$config['mod']['link_shadow_restore'] = '[SD Restore]';
$config['mod']['link_shadow_delete'] = '[SD Delete]';
// Moderator capcodes.
$config['capcode'] = ' <span class="capcode">## %s</span>';
@ -1751,6 +1766,12 @@
$config['mod']['feature_archived_threads'] = JANITOR;
// Delete featured archived threads
$config['mod']['delete_featured_archived_threads'] = MOD;
// View shadow deleted posts and threads
$config['mod']['view_shadow_posts'] = MOD;
// Restore shadow deleted posts and threads
$config['mod']['restore_shadow_post'] = MOD;
// Permanently delete shadow deleted posts and threads
$config['mod']['delete_shadow_post'] = ADMIN;
// Execute un-filtered SQL queries on the database (?/debug/sql)
$config['mod']['debug_sql'] = DISABLED;
// Look through all cache values for debugging when APC is enabled (?/debug/apc)

62
inc/functions.php

@ -227,6 +227,16 @@ function loadConfig() {
$config['uri_img'] = sprintf($config['uri_img'], $board['dir']);
}
if (!isset($config['uri_shadow_thumb']))
$config['uri_shadow_thumb'] = $config['root'] . $board['dir'] . $config['dir']['shadow_del'] . $config['dir']['thumb'];
elseif (isset($board['dir']))
$config['uri_shadow_thumb'] = sprintf($config['uri_shadow_thumb'], $board['dir']);
if (!isset($config['uri_shadow_img']))
$config['uri_shadow_img'] = $config['root'] . $board['dir'] . $config['dir']['shadow_del'] . $config['dir']['img'];
elseif (isset($board['dir']))
$config['uri_shadow_img'] = sprintf($config['uri_shadow_img'], $board['dir']);
if (!isset($config['uri_stylesheets']))
$config['uri_stylesheets'] = $config['root'] . 'stylesheets/';
@ -1419,8 +1429,26 @@ function rebuildPost($id) {
return true;
}
// Delete a post (reply or thread)
function deletePostShadow($id, $error_if_doesnt_exist=true, $rebuild_after=true) {
global $board, $config;
// If we are using non permanent delete run that function
if($config['shadow_del']['use'])
return ShadowDelete::deletePost($id, $error_if_doesnt_exist, $rebuild_after);
else
return deletePostPermanent($id, $error_if_doesnt_exist, $rebuild_after);
}
// Delete a post (reply or thread)
function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) {
function deletePostPermanent($id, $error_if_doesnt_exist=true, $rebuild_after=true) {
global $board, $config;
// Select post and replies (if thread) in one query
@ -1534,10 +1562,10 @@ function clean($pid = false) {
while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
if($config['archive']['threads']) {
Archive::archiveThread($post['id']);
deletePost($post['id'], false, false);
deletePostPermanent($post['id'], false, false);
if ($pid) modLog("Automatically archived thread #{$post['id']} due to new thread #{$pid}");
} else {
deletePost($post['id'], false, false);
deletePostPermanent($post['id'], false, false);
if ($pid) modLog("Automatically deleting thread #{$post['id']} due to new thread #{$pid}");
}
}
@ -1561,10 +1589,12 @@ function clean($pid = false) {
if ($post['reply_count'] < $page*$config['early_404_replies']) {
if($config['archive']['threads']) {
Archive::archiveThread($post['thread_id']);
deletePostPermanent($post['thread_id'], false, false);
if ($pid) modLog("Automatically archived thread #{$post['thread_id']} due to new thread #{$pid} (early 404 is set, #{$post['thread_id']} had {$post['reply_count']} replies)");
} else {
deletePostPermanent($post['thread_id'], false, false);
if ($pid) modLog("Automatically deleting thread #{$post['thread_id']} due to new thread #{$pid} (early 404 is set, #{$post['thread_id']} had {$post['reply_count']} replies)");
}
deletePost($post['thread_id'], false, false);
if ($pid) modLog("Automatically deleting thread #{$post['thread_id']} due to new thread #{$pid} (early 404 is set, #{$post['thread_id']} had {$post['reply_count']} replies)");
}
if ($config['early_404_staged']) {
@ -2615,7 +2645,7 @@ function strip_combining_chars($str) {
return $str;
}
function buildThread($id, $return = false, $mod = false) {
function buildThread($id, $return = false, $mod = false, $shadow = false) {
global $board, $config, $build_pages;
$id = round($id);
@ -2634,11 +2664,29 @@ function buildThread($id, $return = false, $mod = false) {
$action = generation_strategy('sb_thread', array($board['uri'], $id));
if ($action == 'rebuild' || $return || $mod) {
$query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id ORDER BY `thread`,`id`", $board['uri']));
$query = prepare(
sprintf("SELECT ``posts_%s``.*, 0 AS `shadow` FROM ``posts_%s`` WHERE (`thread` IS NULL AND `id` = :id) OR `thread` = :id", $board['uri'], $board['uri']) .
($shadow?" UNION ALL " . sprintf("SELECT ``shadow_posts_%s``.*, 1 AS `shadow` FROM ``shadow_posts_%s`` WHERE `thread` = :id", $board['uri'], $board['uri']):"") .
" ORDER BY `thread`,`id`");
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
// Fix Filenames if shadow copy
if($post['shadow'] && $post['files']) {
$files_new = array();
// Move files to temp storage
foreach (json_decode($post['files']) as $i => $f) {
if ($f->file !== 'deleted') {
// Add file to array of all files
$f->file = ShadowDelete::hashShadowDelFilename($f->file);
$f->thumb = ShadowDelete::hashShadowDelFilename($f->thumb);
$files_new[] = $f;
}
}
$post['files'] = json_encode($files_new);
}
if (!isset($thread)) {
$thread = new Thread($post, $mod ? '?/' : $config['root'], $mod);
} else {

119
inc/mod/pages.php

@ -798,7 +798,11 @@ function mod_view_thread($boardName, $thread) {
if (!openBoard($boardName))
error($config['error']['noboard']);
$page = buildThread($thread, true, $mod);
// Purge shadow posts that have timed out
if($config['shadow_del']['use'])
ShadowDelete::purge();
$page = buildThread($thread, true, $mod, hasPermission($config['mod']['view_shadow_posts']));
echo $page;
}
@ -1349,7 +1353,7 @@ function mod_move_reply($originBoard, $postID) {
openBoard($originBoard);
// delete original post
deletePost($postID);
deletePostPermanent($postID);
buildIndex();
// open target board for redirect
@ -1603,7 +1607,7 @@ function mod_move($originBoard, $postID) {
header('Location: ?/' . sprintf($config['board_path'], $newboard['uri']) . $config['dir']['res'] . link_for($op, false, $newboard) .
'#' . $botID, true, $config['redirect_http']);
} else {
deletePost($postID);
deletePostPermanent($postID);
buildIndex();
openBoard($targetBoard);
@ -1817,7 +1821,7 @@ function mod_merge($originBoard, $postID) {
// return to original board
openBoard($originBoard);
deletePost($postID);
deletePostPermanent($postID);
buildIndex();
openBoard($targetBoard);
@ -1893,7 +1897,6 @@ function mod_ban_post($board, $delete, $post, $token = false) {
buildThread($thread ? $thread : $post);
buildIndex();
} elseif (isset($_POST['delete']) && (int) $_POST['delete']) {
// Delete post
if ($config['autotagging']){
$query = prepare(sprintf("SELECT * FROM ``posts_%s`` WHERE id = :id", $board));
$query->bindValue(':id', $post );
@ -1934,8 +1937,9 @@ function mod_ban_post($board, $delete, $post, $token = false) {
$query->execute() or error(db_error($query));
modLog("Added a note for <a href=\"?/IP/{$ip}\">{$ip}</a>");
}
}
deletePost($post);
}
// Delete post
deletePostShadow($post);
modLog("Deleted post #{$post}");
// Rebuild board
buildIndex();
@ -2007,7 +2011,7 @@ function mod_warning_post($board, $delete, $post, $token = false) {
error($config['error']['noaccess']);
// Delete post
deletePost($post);
deletePostPermanent($post);
modLog("Deleted post #{$post}");
// Rebuild board
buildIndex();
@ -2114,6 +2118,98 @@ function mod_edit_post($board, $edit_raw_html, $postID) {
}
}
function mod_shadow_restore_post($board, $post) {
global $config, $mod;
if (!openBoard($board))
error($config['error']['noboard']);
if (!hasPermission($config['mod']['restore_shadow_post'], $board))
error($config['error']['noaccess']);
// Restore Post
$thread_id = ShadowDelete::restorePost($post);
// Record the action
modLog("Restored Shadow Deleted post #{$post}");
// Rebuild board
buildIndex();
// Rebuild thread
buildThread($thread_id ? $thread_id : $post);
// Rebuild themes
rebuildThemes('post-delete', $board);
// Redirect
if($thread_id !== true) {
// If we got a thread id number as response reload to thread
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['dir']['res'] . sprintf($config['file_page'], $thread_id), true, $config['redirect_http']);
} else {
// Reload to board index
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
}
}
function mod_shadow_delete_post($board, $post) {
global $config, $mod;
if (!openBoard($board))
error($config['error']['noboard']);
if (!hasPermission($config['mod']['delete_shadow_post'], $board))
error($config['error']['noaccess']);
// Restore Post
$thread_id = ShadowDelete::purgePost($post);
// Record the action
modLog("Permanently Deleted Shadow Deleted post #{$post}");
// Redirect
if($thread_id !== true) {
// If we got a thread id number as response reload to thread
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['dir']['res'] . sprintf($config['file_page'], $thread_id), true, $config['redirect_http']);
} else {
// Reload to board index
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
}
}
function mod_shadow_purge() {
global $config, $mod;
if (!hasPermission($config['mod']['purge_shadow_posts'], $board))
error($config['error']['noaccess']);
// Restore Post
$thread_id = ShadowDelete::purgePost($post);
// Record the action
modLog("Permanently Deleted Shadow Deleted post #{$post}");
// Redirect
if($thread_id !== true) {
// If we got a thread id number as response reload to thread
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['dir']['res'] . sprintf($config['file_page'], $thread_id), true, $config['redirect_http']);
} else {
// Reload to board index
header('Location: ?/' . sprintf($config['board_path'], $board) . $config['file_index'], true, $config['redirect_http']);
}
}
function mod_delete($board, $post) {
global $config, $mod;
@ -2164,8 +2260,9 @@ function mod_delete($board, $post) {
$query->execute() or error(db_error($query));
modLog("Added a note for <a href=\"?/IP/{$ip}\">{$ip}</a>");
}
}
deletePost($post);
}
// Delete post (get thread id)
$thread_id = deletePostShadow($post);
// Record the action
modLog("Deleted post #{$post}");
// Rebuild board
@ -2352,7 +2449,7 @@ function mod_deletebyip($boardName, $post, $global = false) {
}
}
deletePost($post['id'], false, false);
deletePostShadow($post['id'], false, false);
rebuildThemes('post-delete', $board['uri']);

418
inc/shadow-delete.php

@ -0,0 +1,418 @@
<?php
class ShadowDelete {
static public function hashShadowDelFilename($filename)
{
global $config;
$file = pathinfo($filename);
return sha1($file['filename'] . $config['shadow_del']['filename_seed']) . "." . $file['extension'];
}
// Delete a post (reply or thread)
static public function deletePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) {
global $board, $config;
// Select post and replies (if thread) in one query
$query = prepare(sprintf("SELECT `id`,`thread`,`files`,`slug` FROM ``posts_%s`` WHERE `id` = :id OR `thread` = :id", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
if ($query->rowCount() < 1) {
if ($error_if_doesnt_exist)
error($config['error']['invalidpost']);
else return false;
}
$ids = array();
$files = array();
// Temporarly Delete posts and maybe replies
while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
event('shadow-delete', $post);
// If thread
if (!$post['thread']) {
// Delete thread HTML page
file_unlink($board['dir'] . $config['dir']['res'] . link_for($post) );
file_unlink($board['dir'] . $config['dir']['res'] . link_for($post, true) ); // noko50
file_unlink($board['dir'] . $config['dir']['res'] . sprintf('%d.json', $post['id']));
// Insert antispam to temp table
$antispam_query = prepare("INSERT INTO ``shadow_antispam`` SELECT * FROM ``antispam`` WHERE `board` = :board AND `thread` = :thread");
$antispam_query->bindValue(':board', $board['uri']);
$antispam_query->bindValue(':thread', $post['id']);
$antispam_query->execute() or error(db_error($antispam_query));
// Delete Antispam entry
$antispam_query = prepare('DELETE FROM ``antispam`` WHERE `board` = :board AND `thread` = :thread');
$antispam_query->bindValue(':board', $board['uri']);
$antispam_query->bindValue(':thread', $post['id']);
$antispam_query->execute() or error(db_error($antispam_query));
} elseif ($query->rowCount() == 1) {
// Rebuild thread
$rebuild = &$post['thread'];
}
if ($post['files']) {
// Move files to temp storage
foreach (json_decode($post['files']) as $i => $f) {
if ($f->file !== 'deleted') {
// Add file to array of all files
$files[] = $f;
// Move files to temp storage
@rename($board['dir'] . $config['dir']['img'] . $f->file, $board['dir'] . $config['dir']['shadow_del'] . $config['dir']['img'] . self::hashShadowDelFilename($f->file));
@rename($board['dir'] . $config['dir']['thumb'] . $f->thumb, $board['dir'] . $config['dir']['shadow_del'] . $config['dir']['thumb'] . self::hashShadowDelFilename($f->thumb));
}
}
}
$ids[] = (int)$post['id'];
}
// Determine if it is an thread or just post we are deleting
$query = prepare(sprintf("SELECT `thread` FROM ``posts_%s`` WHERE `id` = :id", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
$thread_id = $query->fetch(PDO::FETCH_ASSOC)['thread'];
// Insert data into temp table
$insert_query = prepare("INSERT INTO ``shadow_deleted`` VALUES(NULL, :board, :post_id, :del_time, :files, :cite_ids)");
$insert_query->bindValue(':board', $board['uri'], PDO::PARAM_STR);
$insert_query->bindValue(':post_id', $id, PDO::PARAM_INT);
$insert_query->bindValue(':del_time', time(), PDO::PARAM_INT);
$insert_query->bindValue(':files', json_encode($files));
$insert_query->bindValue(':cite_ids', json_encode($ids));
$insert_query->execute() or error(db_error($insert_query));
// Insert post table into temp post table
$insert_query = prepare(sprintf("INSERT INTO ``shadow_posts_%s`` SELECT * FROM ``posts_%s`` WHERE `id` = " . implode(' OR `id` = ', $ids), $board['uri'], $board['uri']));
$insert_query->execute() or error(db_error($insert_query));
// Delete post table entries
$query = prepare(sprintf("DELETE FROM ``posts_%s`` WHERE `id` = :id OR `thread` = :id", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
// Insert filehash table into temp filehash table
$insert_query = prepare("INSERT INTO ``shadow_filehashes`` SELECT * FROM ``filehashes`` WHERE `board` = :board AND (`post` = " . implode(' OR `post` = ', $ids) . ")");
$insert_query->bindValue(':board', $board['uri'], PDO::PARAM_STR);
$insert_query->execute() or error(db_error($insert_query));
// Delete filehash entries for thread from filehash table
$query = prepare(sprintf("DELETE FROM ``filehashes`` WHERE ( `thread` = :id OR `post` = :id ) AND `board` = '%s'", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
// Update bump order
if (isset($thread_id))
{
$query = prepare(sprintf('SELECT MAX(`time`) AS `correct_bump` FROM `posts_%s` WHERE (`thread` = :thread AND NOT email <=> "sage") OR `id` = :thread', $board['uri']));
$query->bindValue(':thread', $thread_id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
$correct_bump = $query->fetch(PDO::FETCH_ASSOC)['correct_bump'];
$query = prepare(sprintf("UPDATE ``posts_%s`` SET `bump` = :bump WHERE `id` = :id", $board['uri']));
$query->bindValue(':bump', $correct_bump, PDO::PARAM_INT);
$query->bindValue(':id', $thread_id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
}
// Update Cite Links
$query = prepare("SELECT `board`, `post` FROM ``cites`` WHERE `target_board` = :board AND (`target` = " . implode(' OR `target` = ', $ids) . ") ORDER BY `board`");
$query->bindValue(':board', $board['uri']);
$query->execute() or error(db_error($query));
while ($cite = $query->fetch(PDO::FETCH_ASSOC)) {
if ($board['uri'] != $cite['board']) {
if (!isset($tmp_board))
$tmp_board = $board['uri'];
openBoard($cite['board']);
}
rebuildPost($cite['post']);
}
if (isset($tmp_board))
openBoard($tmp_board);
// Insert Cited to temp table
$query = prepare("INSERT INTO ``shadow_cites`` SELECT * FROM ``cites`` WHERE (`target_board` = :board AND (`target` = " . implode(' OR `target` = ', $ids) . ")) OR (`board` = :board AND (`post` = " . implode(' OR `post` = ', $ids) . "))");
$query->bindValue(':board', $board['uri']);
$query->execute() or error(db_error($query));
// Delete Cites
$query = prepare("DELETE FROM ``cites`` WHERE (`target_board` = :board AND (`target` = " . implode(' OR `target` = ', $ids) . ")) OR (`board` = :board AND (`post` = " . implode(' OR `post` = ', $ids) . "))");
$query->bindValue(':board', $board['uri']);
$query->execute() or error(db_error($query));
if (isset($rebuild) && $rebuild_after) {
buildThread($rebuild);
buildIndex();
}
// If Thread ID is set return it (deleted post within thread) this will pe a positive number and thus viewed as true for legacy purposes
if(isset($thread_id))
return $thread_id;
return true;
}
// Restore a post (reply or thread)
static public function restorePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) {
global $board, $config;
// Select post and replies (if thread) in one query
$query = prepare(sprintf("SELECT `id`,`thread`,`files`,`slug` FROM ``shadow_posts_%s`` WHERE `id` = :id OR `thread` = :id", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
if ($query->rowCount() < 1) {
if ($error_if_doesnt_exist)
error($config['error']['invalidpost']);
else return false;
}
$ids = array();
// Restore posts and maybe replies
while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
event('shadow-restore', $post);
// If thread
if (!$post['thread']) {
// Insert temp antispam to table
$antispam_query = prepare("INSERT INTO ``antispam`` SELECT * FROM ``shadow_antispam`` WHERE `board` = :board AND `thread` = :thread");
$antispam_query->bindValue(':board', $board['uri']);
$antispam_query->bindValue(':thread', $post['id']);
$antispam_query->execute() or error(db_error($antispam_query));
// Delete Temp Antispam entry
$antispam_query = prepare('DELETE FROM ``shadow_antispam`` WHERE `board` = :board AND `thread` = :thread');
$antispam_query->bindValue(':board', $board['uri']);
$antispam_query->bindValue(':thread', $post['id']);
$antispam_query->execute() or error(db_error($antispam_query));
}
// Restore Files
if ($post['files']) {
// Move files from temp storage
foreach (json_decode($post['files']) as $i => $f) {
if ($f->file !== 'deleted') {
@rename($board['dir'] . $config['dir']['shadow_del'] . $config['dir']['img'] . self::hashShadowDelFilename($f->file), $board['dir'] . $config['dir']['img'] . $f->file);
@rename($board['dir'] . $config['dir']['shadow_del'] . $config['dir']['thumb'] . self::hashShadowDelFilename($f->thumb), $board['dir'] . $config['dir']['thumb'] . $f->thumb);
}
}
}
$ids[] = (int)$post['id'];
}
// Delete data from temp table
$insert_query = prepare("DELETE FROM ``shadow_deleted`` WHERE `board` = :board AND `post_id` = :id");
$insert_query->bindValue(':board', $board['uri'], PDO::PARAM_STR);
$insert_query->bindValue(':id', $post['id'], PDO::PARAM_INT);
$insert_query->execute() or error(db_error($insert_query));
// Determin if it is an thread or just post we are restoring
$query = prepare(sprintf("SELECT `thread` FROM ``shadow_posts_%s`` WHERE `id` = :id", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
$thread_id = $query->fetch(PDO::FETCH_ASSOC)['thread'];
// Insert temp post table into post table
$insert_query = prepare(sprintf("INSERT INTO ``posts_%s`` SELECT * FROM ``shadow_posts_%s`` WHERE `id` = " . implode(' OR `id` = ', $ids), $board['uri'], $board['uri']));
$insert_query->execute() or error(db_error($insert_query));
// Delete post table entries
$query = prepare(sprintf("DELETE FROM ``shadow_posts_%s`` WHERE `id` = :id OR `thread` = :id", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
// Insert filehash table into temp filehash table
$insert_query = prepare("INSERT INTO ``filehashes`` SELECT * FROM ``shadow_filehashes`` WHERE `board` = :board AND (`post` = " . implode(' OR `post` = ', $ids) . ")");
$insert_query->bindValue(':board', $board['uri'], PDO::PARAM_STR);
$insert_query->execute() or error(db_error($insert_query));
// Delete filehash entries for thread from filehash table
$query = prepare(sprintf("DELETE FROM ``shadow_filehashes`` WHERE ( `thread` = :id OR `post` = :id ) AND `board` = '%s'", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
// Update bump order
if (isset($thread_id))
{
$query = prepare(sprintf('SELECT MAX(`time`) AS `correct_bump` FROM `posts_%s` WHERE (`thread` = :thread AND NOT email <=> "sage") OR `id` = :thread', $board['uri']));
$query->bindValue(':thread', $thread_id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
$correct_bump = $query->fetch(PDO::FETCH_ASSOC)['correct_bump'];
$query = prepare(sprintf("UPDATE ``posts_%s`` SET `bump` = :bump WHERE `id` = :id", $board['uri']));
$query->bindValue(':bump', $correct_bump, PDO::PARAM_INT);
$query->bindValue(':id', $thread_id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
}
$query = prepare("SELECT `board`, `post` FROM ``shadow_cites`` WHERE `target_board` = :board AND (`target` = " . implode(' OR `target` = ', $ids) . ") ORDER BY `board`");
$query->bindValue(':board', $board['uri']);
$query->execute() or error(db_error($query));
while ($cite = $query->fetch(PDO::FETCH_ASSOC)) {
if ($board['uri'] != $cite['board']) {
if (!isset($tmp_board))
$tmp_board = $board['uri'];
openBoard($cite['board']);
}
rebuildPost($cite['post']);
}
if (isset($tmp_board))
openBoard($tmp_board);
// Insert Temp Cited to Cited Table
$query = prepare("INSERT INTO ``cites`` SELECT * FROM ``shadow_cites`` WHERE (`target_board` = :board AND (`target` = " . implode(' OR `target` = ', $ids) . ")) OR (`board` = :board AND (`post` = " . implode(' OR `post` = ', $ids) . "))");
$query->bindValue(':board', $board['uri']);
$query->execute() or error(db_error($query));
// Delete Temp Cites
$query = prepare("DELETE FROM ``shadow_cites`` WHERE (`target_board` = :board AND (`target` = " . implode(' OR `target` = ', $ids) . ")) OR (`board` = :board AND (`post` = " . implode(' OR `post` = ', $ids) . "))");
$query->bindValue(':board', $board['uri']);
$query->execute() or error(db_error($query));
if (isset($rebuild) && $rebuild_after) {
buildThread($rebuild);
buildIndex();
}
// If Thread ID is set return it (deleted post within thread) this will pe a positive number and thus viewed as true for legacy purposes
if(isset($thread_id))
return $thread_id;
return true;
}
// Purge a post (reply or thread)
static public function purgePost($id, $error_if_doesnt_exist=true, $rebuild_after=true) {
global $board, $config;
// Select post and replies (if thread) in one query
$query = prepare(sprintf("SELECT `id`,`thread`,`files`,`slug` FROM ``shadow_posts_%s`` WHERE `id` = :id OR `thread` = :id", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
if ($query->rowCount() < 1) {
if ($error_if_doesnt_exist)
error($config['error']['invalidpost']);
else return false;
}
$ids = array();
// Delete files
while ($post = $query->fetch(PDO::FETCH_ASSOC)) {
event('shadow-perm-delete', $post);
if ($post['files']) {
foreach (json_decode($post['files']) as $i => $f) {
if ($f->file !== 'deleted') {
@unlink($board['dir'] . $config['dir']['shadow_del'] . $config['dir']['img'] . self::hashShadowDelFilename($f->file));
@unlink($board['dir'] . $config['dir']['shadow_del'] . $config['dir']['thumb'] . self::hashShadowDelFilename($f->thumb));
}
}
}
$ids[] = (int)$post['id'];
}
// Delete data from temp table
$insert_query = prepare("DELETE FROM ``shadow_deleted`` WHERE `board` = :board AND `post_id` = :id");
$insert_query->bindValue(':board', $board['uri'], PDO::PARAM_STR);
$insert_query->bindValue(':id', $post['id'], PDO::PARAM_INT);
$insert_query->execute() or error(db_error($insert_query));
// Determin if it is an thread or just post we are restoring
$query = prepare(sprintf("SELECT `thread` FROM ``shadow_posts_%s`` WHERE `id` = :id", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
$thread_id = $query->fetch(PDO::FETCH_ASSOC)['thread'];
// Delete post table entries
$query = prepare(sprintf("DELETE FROM ``shadow_posts_%s`` WHERE `id` = :id OR `thread` = :id", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
// Delete filehash entries for thread from filehash table
$query = prepare(sprintf("DELETE FROM ``shadow_filehashes`` WHERE ( `thread` = :id OR `post` = :id ) AND `board` = '%s'", $board['uri']));
$query->bindValue(':id', $id, PDO::PARAM_INT);
$query->execute() or error(db_error($query));
// Delete Temp Cites
$query = prepare("DELETE FROM ``shadow_cites`` WHERE (`target_board` = :board AND (`target` = " . implode(' OR `target` = ', $ids) . ")) OR (`board` = :board AND (`post` = " . implode(' OR `post` = ', $ids) . "))");
$query->bindValue(':board', $board['uri']);
$query->execute() or error(db_error($query));
// If Thread ID is set return it (deleted post within thread) this will pe a positive number and thus viewed as true for legacy purposes
if(isset($thread_id))
return $thread_id;
return true;
}
static public function purge() {
global $config;
// Delete data from temp table
$query = prepare("SELECT * FROM ``shadow_deleted`` WHERE `del_time` < :del_time");
$query->bindValue(':del_time', strtotime("-" . $config['shadow_del']['lifetime']), PDO::PARAM_INT);
$query->execute() or error(db_error($query));
// Temporarly Delete posts and maybe replies
while ($shadow_post = $query->fetch(PDO::FETCH_ASSOC)) {
event('shadow-perm-delete', $shadow_post);
// Set Board Dir for Deletion
$board['dir'] = sprintf($config['board_path'], $shadow_post['board']);
// Delete files from temp storage
foreach (json_decode($shadow_post['files']) as $i => $f) {
@unlink($board['dir'] . $config['dir']['shadow_del'] . $config['dir']['img'] . self::hashShadowDelFilename($f->file));
@unlink($board['dir'] . $config['dir']['shadow_del'] . $config['dir']['thumb'] . self::hashShadowDelFilename($f->thumb));
}
// Delete post table entries
$delete_query = prepare(sprintf("DELETE FROM ``shadow_posts_%s`` WHERE `id` = :id OR `thread` = :id", $shadow_post['board']));
$delete_query->bindValue(':id', $shadow_post['post_id'], PDO::PARAM_INT);
$delete_query->execute() or error(db_error($delete_query));
// Delete filehash entries for thread from filehash table
$delete_query = prepare("DELETE FROM ``shadow_filehashes`` WHERE ( `thread` = :id OR `post` = :id ) AND `board` = :board");
$delete_query->bindValue(':id', $shadow_post['post_id'], PDO::PARAM_INT);
$delete_query->bindValue(':board', $shadow_post['board'], PDO::PARAM_STR);
$delete_query->execute() or error(db_error($delete_query));
// Delete Temp Antispam entry
$delete_query = prepare('DELETE FROM ``shadow_antispam`` WHERE `board` = :board AND `thread` = :thread');
$delete_query->bindValue(':board', $shadow_post['board']);
$delete_query->bindValue(':thread', $shadow_post['post_id']);
$delete_query->execute() or error(db_error($delete_query));
// Delete Temp Cites
$ids = array();
foreach (json_decode($shadow_post['cite_ids']) as $c)
$ids[] = $c;
// Delete Temp Cites
$delete_query = prepare("DELETE FROM ``cites`` WHERE (`target_board` = :board AND (`target` = " . implode(' OR `target` = ', $ids) . ")) OR (`board` = :board AND (`post` = " . implode(' OR `post` = ', $ids) . "))");
$delete_query->bindValue(':board', $board['uri']);
$delete_query->execute() or error(db_error($delete_query));
}
// Delete data from temp table
$query = prepare("DELETE FROM ``shadow_deleted`` WHERE `del_time` < :del_time");
$query->bindValue(':del_time', strtotime("-" . $config['shadow_del']['lifetime']), PDO::PARAM_INT);
$query->execute() or error(db_error($query));
return true;
}
}
?>

7
mod.php

@ -77,7 +77,12 @@ $pages = array(
'/search/(posts|IP_notes|bans|log)/(.+)' => 'search', // search
'/(\%b)/archive/' => 'secure_POST view_archive', // view archive
'/(\%b)/featured/' => 'secure_POST view_archive_featured', // view featured Archive
'/(\%b)/featured/' => 'secure_POST view_archive_featured', // view featured archive
'/(\%b)/shadow_restore/(\d+)' => 'secure_POST shadow_restore_post', // restore shadow deleted post
'/(\%b)/shadow_delete/(\d+)' => 'secure_POST shadow_delete_post', // permanent delete shadow deleted post
'/(\%b)/shadow_purge/(\d+)' => 'secure_POST shadow_purge', // permanent delete all shadow deleted post that have timed out
'/(\%b)/warning(&delete)?/(\d+)' => 'secure_POST warning_post', // issue warning for post
'/(\%b)/ban(&delete)?/(\d+)' => 'secure_POST ban_post', // ban poster
'/(\%b)/move/(\d+)' => 'secure_POST move', // move thread

2
post.php

@ -248,7 +248,7 @@ if (isset($_POST['delete'])) {
}
// Delete entire post
deletePost($id);
deletePostPermanent($id);
modLog("User deleted his own post #$id");
}

6
stylesheets/style.css

@ -377,6 +377,12 @@ span.spoiler {
padding: 0 1px;
}
.shadow-post {
filter: grayscale(80%) !important;
opacity: 0.6 !important; /* Real browsers */
filter: alpha(opacity = 60) !important; /* MSIE */
}
div.post.reply div.body span.spoiler a {
color: black;
}

2
templates/post/file_controls.html

@ -1,4 +1,5 @@
{% if post.files and mod %}
{% if not post.shadow %}
<span class="controls">
{% if file.file != 'deleted' and mod|hasPermission(config.mod.deletefile, board.uri) %}
{{ secure_link_confirm(config.mod.link_deletefile, 'Delete file'|trans, 'Are you sure you want to delete this file?'|trans, board.dir ~ 'deletefile/' ~ post.id ~ '/' ~ loop.index0 ) }}&nbsp;
@ -11,3 +12,4 @@
{% endif %}
</span>
{% endif %}
{% endif %}

4
templates/post/fileinfo.html

@ -7,7 +7,7 @@
{% if file.file == 'deleted' %}
<img class="post-image deleted" src="{{ config.root }}{{ config.image_deleted }}" alt="" />
{% else %}
<p class="fileinfo">File: <a href="{{ config.uri_img }}{{ file.file }}" target="_blank">{{ file.file }}</a> <span class="unimportant">
<p class="fileinfo">File: <a href="{% if post.shadow %}{{ config.uri_shadow_img }}{% else %}{{ config.uri_img }}{% endif %}{{ file.file }}" target="_blank">>{{ file.file }}</a> <span class="unimportant">
(
{% if file.thumb == 'spoiler' %}
{% trans %}Spoiler Image{% endtrans %},
@ -30,7 +30,7 @@
)
{% include "post/image_identification.html" %}
{% include "post/file_controls.html" %}</span></p>
{% include "post/image.html" with {'post':file} %}
{% include "post/image.html" with {'shadow':post.shadow, 'post':file} %}
{% endif %}
</div>
{% endfor %}

8
templates/post/image.html

@ -1,8 +1,8 @@
<a href="
{% if post.file|extension == 'webm' or post.file|extension == 'mp4' %}
{{ config.root }}player.php?v={{ config.uri_img }}{{ post.file }}&amp;t={{ post.filename|e('url') }}&amp;loop=1
{{ config.root }}player.php?v={% if shadow %}{{ config.uri_shadow_img }}{% else %}{{ config.uri_img }}{% endif %}{{ post.file }}&amp;t={{ post.filename|e('url') }}&amp;loop=1
{% else %}
{{ config.uri_img }}{{ post.file }}
{% if shadow %}{{ config.uri_shadow_img }}{% else %}{{ config.uri_img }}{% endif %}{{ post.file }}
{% endif %}
"
target="_blank"
@ -11,7 +11,7 @@
{% endif %}
>
{% if post.thumb|extension == 'webm' or post.thumb|extension == 'mp4' %}
<video autoplay class="post-image" src="{{ config.uri_thumb }}{{ post.thumb }}"
<video autoplay class="post-image" src="{% if shadow %}{{ config.uri_shadow_img }}{% else %}{{ config.uri_img }}{% endif %}{{ post.thumb }}"
poster="
{{ config.root }}
{% if config.file_icons[post.filename|extension] %}
@ -36,7 +36,7 @@
{% elseif post.thumb == 'spoiler' %}
{{ config.root }}{{ config.spoiler_image }}
{% else %}
{{ config.uri_thumb }}{{ post.thumb }}
{% if shadow %}{{ config.uri_shadow_thumb }}{% else %}{{ config.uri_thumb }}{% endif %}{{ post.thumb }}
{% endif %}
"
style="width:{{ post.thumbwidth }}px;height:{{ post.thumbheight }}px" alt=""

125
templates/post/post_controls.html

@ -1,71 +1,80 @@
{% if mod %}
<span class="controls {% if not post.thread %}op{% endif %}">
{% if mod|hasPermission(config.mod.delete, board.uri) %}
{{ secure_link_confirm(config.mod.link_delete, 'Delete'|trans, 'Are you sure you want to delete this?'|trans, board.dir ~ 'delete/' ~ post.id) }}&nbsp;
{% endif %}
{% if mod|hasPermission(config.mod.deletebyip, board.uri) %}
{{ secure_link_confirm(config.mod.link_deletebyip, 'Delete all posts by IP'|trans, 'Are you sure you want to delete all posts by this IP address?'|trans, board.dir ~ 'deletebyip/' ~ post.id) }}&nbsp;
{% endif %}
{% if mod|hasPermission(config.mod.deletebyip_global, board.uri) %}
{{ secure_link_confirm(config.mod.link_deletebyip_global, 'Delete all posts by IP across all boards'|trans, 'Are you sure you want to delete all posts by this IP address, across all boards?'|trans, board.dir ~ 'deletebyip/' ~ post.id ~ '/global') }}&nbsp;
{% endif %}
{% if mod|hasPermission(config.mod.warning, board.uri) %}
<a title="{% trans %}Issue Warning{% endtrans %}" href="?/{{ board.dir }}warning/{{ post.id }}">{{ config.mod.link_warning }}</a>&nbsp;
{% endif %}
{% if mod|hasPermission(config.mod.warning, board.uri) and mod|hasPermission(config.mod.delete, board.uri) %}
<a title="{% trans %}Issue Warning{% endtrans %}" href="?/{{ board.dir }}warning&amp;delete/{{ post.id }}">{{ config.mod.link_warningdelete }}</a>&nbsp;
{% endif %}
{% if mod|hasPermission(config.mod.ban, board.uri) %}
<a title="{% trans %}Ban{% endtrans %}" href="?/{{ board.dir }}ban/{{ post.id }}">{{ config.mod.link_ban }}</a>&nbsp;
{% endif %}
{% if mod|hasPermission(config.mod.bandelete, board.uri) %}
<a title="{% trans %}Ban & Delete{% endtrans %}" href="?/{{ board.dir }}ban&amp;delete/{{ post.id }}">{{ config.mod.link_bandelete }}</a>&nbsp;
{% endif %}
{% if not post.thread %}
{% if mod|hasPermission(config.mod.sticky, board.uri) %}
{% if post.sticky %}
<a title="{% trans %}Make thread not sticky{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'unsticky/' ~ post.id) }}">{{ config.mod.link_desticky }}</a>&nbsp;
{% else %}
<a title="{% trans %}Make thread sticky{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'sticky/' ~ post.id) }}">{{ config.mod.link_sticky }}</a>&nbsp;
{% if not post.shadow %}
{% if mod|hasPermission(config.mod.delete, board.uri) %}
{{ secure_link_confirm(config.mod.link_delete, 'Delete'|trans, 'Are you sure you want to delete this?'|trans, board.dir ~ 'delete/' ~ post.id) }}&nbsp;
{% endif %}
{% endif %}
{% if mod|hasPermission(config.mod.bumplock, board.uri) %}
{% if post.sage %}
<a title="{% trans %}Allow thread to be bumped{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'bumpunlock/' ~ post.id) }}">{{ config.mod.link_bumpunlock }}</a>&nbsp;
{% else %}
<a title="{% trans %}Prevent thread from being bumped{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'bumplock/' ~ post.id) }}">{{ config.mod.link_bumplock }}</a>&nbsp;
{% if mod|hasPermission(config.mod.deletebyip, board.uri) %}
{{ secure_link_confirm(config.mod.link_deletebyip, 'Delete all posts by IP'|trans, 'Are you sure you want to delete all posts by this IP address?'|trans, board.dir ~ 'deletebyip/' ~ post.id) }}&nbsp;
{% endif %}
{% endif %}
{% if mod|hasPermission(config.mod.lock, board.uri) %}
{% if post.locked %}
<a title="{% trans %}Unlock thread{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'unlock/' ~ post.id) }}">{{ config.mod.link_unlock }}</a>&nbsp;
{% else %}
<a title="{% trans %}Lock thread{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'lock/' ~ post.id) }}">{{ config.mod.link_lock }}</a>&nbsp;
{% if mod|hasPermission(config.mod.deletebyip_global, board.uri) %}
{{ secure_link_confirm(config.mod.link_deletebyip_global, 'Delete all posts by IP across all boards'|trans, 'Are you sure you want to delete all posts by this IP address, across all boards?'|trans, board.dir ~ 'deletebyip/' ~ post.id ~ '/global') }}&nbsp;
{% endif %}
{% if mod|hasPermission(config.mod.warning, board.uri) %}
<a title="{% trans %}Issue Warning{% endtrans %}" href="?/{{ board.dir }}warning/{{ post.id }}">{{ config.mod.link_warning }}</a>&nbsp;
{% endif %}
{% if mod|hasPermission(config.mod.warning, board.uri) and mod|hasPermission(config.mod.delete, board.uri) %}
<a title="{% trans %}Issue Warning{% endtrans %}" href="?/{{ board.dir }}warning&amp;delete/{{ post.id }}">{{ config.mod.link_warningdelete }}</a>&nbsp;
{% endif %}
{% if mod|hasPermission(config.mod.ban, board.uri) %}
<a title="{% trans %}Ban{% endtrans %}" href="?/{{ board.dir }}ban/{{ post.id }}">{{ config.mod.link_ban }}</a>&nbsp;
{% endif %}
{% if mod|hasPermission(config.mod.bandelete, board.uri) %}
<a title="{% trans %}Ban & Delete{% endtrans %}" href="?/{{ board.dir }}ban&amp;delete/{{ post.id }}">{{ config.mod.link_bandelete }}</a>&nbsp;
{% endif %}
{% endif %}
{% endif %}
{% if mod|hasPermission(config.mod.move, board.uri) %}
{% if not post.thread %}
<a title="{% trans %}Move thread to another board{% endtrans %}" href="?/{{ board.dir }}move/{{ post.id }}">{{ config.mod.link_move }}</a>&nbsp;
{% else %}
<a title="{% trans %}Move reply to another board{% endtrans %}" href="?/{{ board.dir }}move_reply/{{ post.id }}">{{ config.mod.link_move }}</a>&nbsp;
{% if mod|hasPermission(config.mod.sticky, board.uri) %}
{% if post.sticky %}
<a title="{% trans %}Make thread not sticky{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'unsticky/' ~ post.id) }}">{{ config.mod.link_desticky }}</a>&nbsp;
{% else %}
<a title="{% trans %}Make thread sticky{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'sticky/' ~ post.id) }}">{{ config.mod.link_sticky }}</a>&nbsp;
{% endif %}
{% endif %}
{% endif %}
{% if mod|hasPermission(config.mod.merge, board.uri) %}
<a title="{% trans %}Merge thread with another thread {% endtrans %}" href="?/{{ board.dir }}merge/{{ post.id }}">{{ config.mod.link_merge }}</a>&nbsp;
{% endif %}
{% if mod|hasPermission(config.mod.cycle, board.uri) %}
{% if post.cycle %}
<a title="{% trans %}Make thread not cycle{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'uncycle/' ~ post.id) }}">{{ config.mod.link_uncycle }}</a>&nbsp;
{% else %}
<a title="{% trans %}Make thread cycle{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'cycle/' ~ post.id) }}">{{ config.mod.link_cycle }}</a>&nbsp;
{% if mod|hasPermission(config.mod.merge, board.uri) %}
<a title="{% trans %}Merge thread with another thread {% endtrans %}" href="?/{{ board.dir }}merge/{{ post.id }}">{{ config.mod.link_merge }}</a>&nbsp;
{% endif %}
{% if mod|hasPermission(config.mod.cycle, board.uri) %}
{% if post.cycle %}
<a title="{% trans %}Make thread not cycle{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'uncycle/' ~ post.id) }}">{{ config.mod.link_uncycle }}</a>&nbsp;
{% else %}
<a title="{% trans %}Make thread cycle{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'cycle/' ~ post.id) }}">{{ config.mod.link_cycle }}</a>&nbsp;
{% endif %}
{% endif %}
{% if mod|hasPermission(config.mod.bumplock, board.uri) %}
{% if post.sage %}
<a title="{% trans %}Allow thread to be bumped{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'bumpunlock/' ~ post.id) }}">{{ config.mod.link_bumpunlock }}</a>&nbsp;
{% else %}
<a title="{% trans %}Prevent thread from being bumped{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'bumplock/' ~ post.id) }}">{{ config.mod.link_bumplock }}</a>&nbsp;
{% endif %}
{% endif %}
{% if mod|hasPermission(config.mod.lock, board.uri) %}
{% if post.locked %}
<a title="{% trans %}Unlock thread{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'unlock/' ~ post.id) }}">{{ config.mod.link_unlock }}</a>&nbsp;
{% else %}
<a title="{% trans %}Lock thread{% endtrans %}" href="?/{{ secure_link(board.dir ~ 'lock/' ~ post.id) }}">{{ config.mod.link_lock }}</a>&nbsp;
{% endif %}
{% endif %}
{% endif %}
{% if mod|hasPermission(config.mod.move, board.uri) %}
{% if not post.thread %}
<a title="{% trans %}Move thread to another board{% endtrans %}" href="?/{{ board.dir }}move/{{ post.id }}">{{ config.mod.link_move }}</a>&nbsp;
{% else %}
<a title="{% trans %}Move reply to another board{% endtrans %}" href="?/{{ board.dir }}move_reply/{{ post.id }}">{{ config.mod.link_move }}</a>&nbsp;
{% endif %}
{% endif %}
{% if mod|hasPermission(config.mod.editpost, board.uri) %}
<a title="{% trans %}Edit post{% endtrans %}" href="?/{{ board.dir }}edit{% if config.mod.raw_html_default %}_raw{% endif %}/{{ post.id }}">{{ config.mod.link_editpost }}</a>&nbsp;
{% endif %}
{% else %}
{% if mod|hasPermission(config.mod.restore_shadow_post, board.uri) %}
<a title="{% trans %}Restore Shadow Deleted Post{% endtrans %}" href="?/{{ board.dir }}sd_restore/{{ post.id }}">{{ config.mod.link_sd_restore }}</a>&nbsp;
{% endif %}
{% if mod|hasPermission(config.mod.delete_shadow_post, board.uri) %}
<a title="{% trans %}Permanent Delete Shadow Deleted Post{% endtrans %}" href="?/{{ board.dir }}sd_delete/{{ post.id }}">{{ config.mod.link_sd_delete }}</a>&nbsp;
{% endif %}
{% endif %}
{% if mod|hasPermission(config.mod.editpost, board.uri) %}
<a title="{% trans %}Edit post{% endtrans %}" href="?/{{ board.dir }}edit{% if config.mod.raw_html_default %}_raw{% endif %}/{{ post.id }}">{{ config.mod.link_editpost }}</a>&nbsp;
{% endif %}
</span>

2
templates/post_reply.html

@ -1,6 +1,6 @@
{% filter remove_whitespace %}
{# tabs and new lines will be ignored #}
<div class="post reply" id="reply_{{ post.id }}">
<div class="post reply {% if post.shadow %}shadow-post{% endif %}" id="reply_{{ post.id }}">
<p class="intro">
{% if not index %}<a id="{{ post.id }}" class="post_anchor"></a>{% endif %}
<input type="checkbox" class="delete" name="delete_{{ post.id }}" id="delete_{{ post.id }}" />

Loading…
Cancel
Save