From c353e1284dd7a3eb0f72c1abb2fcb0287f586dcc Mon Sep 17 00:00:00 2001
From: PupperWoff
Date: Fri, 9 Jun 2017 03:59:18 +0200
Subject: [PATCH] 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
---
UPDATE_SCRIPT__SHADOW_DELETE.php | 83 ++++++
composer.json | 1 +
inc/config.php | 21 ++
inc/functions.php | 62 ++++-
inc/mod/pages.php | 119 ++++++++-
inc/shadow-delete.php | 418 ++++++++++++++++++++++++++++++
mod.php | 7 +-
post.php | 2 +-
stylesheets/style.css | 6 +
templates/post/file_controls.html | 2 +
templates/post/fileinfo.html | 4 +-
templates/post/image.html | 8 +-
templates/post/post_controls.html | 125 ++++-----
templates/post_reply.html | 2 +-
14 files changed, 775 insertions(+), 85 deletions(-)
create mode 100644 UPDATE_SCRIPT__SHADOW_DELETE.php
create mode 100644 inc/shadow-delete.php
diff --git a/UPDATE_SCRIPT__SHADOW_DELETE.php b/UPDATE_SCRIPT__SHADOW_DELETE.php
new file mode 100644
index 00000000..a08fce66
--- /dev/null
+++ b/UPDATE_SCRIPT__SHADOW_DELETE.php
@@ -0,0 +1,83 @@
+You are about to update the database to allow temporarely deletion of threads (with thimeout for permanent deletion).
';
+ $page['body'] .= 'Click here to update database entries and folder structure.
';
+ break;
+ case 2:
+ $page['body'] = 'Database have been updated.
';
+
+ $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 .= "Add Shared Shadow Post DB Tables
" . db_error() . '';
+
+ // 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("Add Shadow Post DB for %s
", $_board['uri']) . db_error() . '';
+
+
+ $_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.
";
+ 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.
";
+ 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.
";
+ }
+
+ if (!empty($sql_errors))
+ $page['body'] .= 'SQL errors
SQL errors were encountered when trying to update the database.
The errors encountered were:
';
+ if (!empty($file_errors))
+ $page['body'] .= 'File System errors
File System errors were encountered when trying to create folders.
The errors encountered were:
';
+
+ break;
+}
+
+
+echo Element('page.html', $page);
+
+?>
+
+
diff --git a/composer.json b/composer.json
index 39e121a4..3ea04a2a 100644
--- a/composer.json
+++ b/composer.json
@@ -28,6 +28,7 @@
"inc/polyfill.php",
"inc/announcements.php",
"inc/archive.php",
+ "inc/shadow-delete.php",
"inc/functions.php"
]
},
diff --git a/inc/config.php b/inc/config.php
index ffcdf84d..a59aa628 100644
--- a/inc/config.php
+++ b/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'] = ' ## %s';
@@ -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)
diff --git a/inc/functions.php b/inc/functions.php
index f5536355..7e33a9c5 100755
--- a/inc/functions.php
+++ b/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 {
diff --git a/inc/mod/pages.php b/inc/mod/pages.php
index 209d8b86..472b1e56 100644
--- a/inc/mod/pages.php
+++ b/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 {$ip}");
}
- }
- 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 {$ip}");
}
- }
- 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']);
diff --git a/inc/shadow-delete.php b/inc/shadow-delete.php
new file mode 100644
index 00000000..5e8487ee
--- /dev/null
+++ b/inc/shadow-delete.php
@@ -0,0 +1,418 @@
+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;
+ }
+}
+?>
\ No newline at end of file
diff --git a/mod.php b/mod.php
index a305beb4..6630f9b1 100644
--- a/mod.php
+++ b/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
diff --git a/post.php b/post.php
index 7025e698..a70d2bd7 100644
--- a/post.php
+++ b/post.php
@@ -248,7 +248,7 @@ if (isset($_POST['delete'])) {
}
// Delete entire post
- deletePost($id);
+ deletePostPermanent($id);
modLog("User deleted his own post #$id");
}
diff --git a/stylesheets/style.css b/stylesheets/style.css
index 16f85664..4319d804 100644
--- a/stylesheets/style.css
+++ b/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;
}
diff --git a/templates/post/file_controls.html b/templates/post/file_controls.html
index 36011aea..307dd8f1 100644
--- a/templates/post/file_controls.html
+++ b/templates/post/file_controls.html
@@ -1,4 +1,5 @@
{% if post.files and mod %}
+{% if not post.shadow %}
{% 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 ) }}
@@ -11,3 +12,4 @@
{% endif %}
{% endif %}
+{% endif %}
diff --git a/templates/post/fileinfo.html b/templates/post/fileinfo.html
index 31a1900c..5d1e0f5f 100644
--- a/templates/post/fileinfo.html
+++ b/templates/post/fileinfo.html
@@ -7,7 +7,7 @@
{% if file.file == 'deleted' %}
{% else %}
- File: {{ file.file }}
+ File: >{{ file.file }}
(
{% if file.thumb == 'spoiler' %}
{% trans %}Spoiler Image{% endtrans %},
@@ -30,7 +30,7 @@
)
{% include "post/image_identification.html" %}
{% include "post/file_controls.html" %}
- {% include "post/image.html" with {'post':file} %}
+ {% include "post/image.html" with {'shadow':post.shadow, 'post':file} %}
{% endif %}
{% endfor %}
diff --git a/templates/post/image.html b/templates/post/image.html
index 16fe20b6..46939404 100644
--- a/templates/post/image.html
+++ b/templates/post/image.html
@@ -1,8 +1,8 @@
{% if post.thumb|extension == 'webm' or post.thumb|extension == 'mp4' %}
-
diff --git a/templates/post_reply.html b/templates/post_reply.html
index 9a6cc392..c4521199 100644
--- a/templates/post_reply.html
+++ b/templates/post_reply.html
@@ -1,6 +1,6 @@
{% filter remove_whitespace %}
{# tabs and new lines will be ignored #}
-