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' %} - {{ config.mod.link_warning }}  -{% endif %} -{% if mod|hasPermission(config.mod.warning, board.uri) and mod|hasPermission(config.mod.delete, board.uri) %} - {{ config.mod.link_warningdelete }}  -{% endif %} -{% if mod|hasPermission(config.mod.ban, board.uri) %} - {{ config.mod.link_ban }}  -{% endif %} -{% if mod|hasPermission(config.mod.bandelete, board.uri) %} - {{ config.mod.link_bandelete }}  -{% endif %} - -{% if not post.thread %} -{% if mod|hasPermission(config.mod.sticky, board.uri) %} - {% if post.sticky %} - {{ config.mod.link_desticky }}  - {% else %} - {{ config.mod.link_sticky }}  +{% 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) }}  {% endif %} -{% endif %} -{% if mod|hasPermission(config.mod.bumplock, board.uri) %} - {% if post.sage %} - {{ config.mod.link_bumpunlock }}  - {% else %} - {{ config.mod.link_bumplock }}  + {% 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) }}  {% endif %} -{% endif %} -{% if mod|hasPermission(config.mod.lock, board.uri) %} - {% if post.locked %} - {{ config.mod.link_unlock }}  - {% else %} - {{ config.mod.link_lock }}  + {% 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') }}  + {% endif %} + {% if mod|hasPermission(config.mod.warning, board.uri) %} + {{ config.mod.link_warning }}  + {% endif %} + {% if mod|hasPermission(config.mod.warning, board.uri) and mod|hasPermission(config.mod.delete, board.uri) %} + {{ config.mod.link_warningdelete }}  + {% endif %} + {% if mod|hasPermission(config.mod.ban, board.uri) %} + {{ config.mod.link_ban }}  + {% endif %} + {% if mod|hasPermission(config.mod.bandelete, board.uri) %} + {{ config.mod.link_bandelete }}  {% endif %} -{% endif %} -{% endif %} -{% if mod|hasPermission(config.mod.move, board.uri) %} {% if not post.thread %} - {{ config.mod.link_move }}  - {% else %} - {{ config.mod.link_move }}  + {% if mod|hasPermission(config.mod.sticky, board.uri) %} + {% if post.sticky %} + {{ config.mod.link_desticky }}  + {% else %} + {{ config.mod.link_sticky }}  + {% endif %} {% endif %} -{% endif %} -{% if mod|hasPermission(config.mod.merge, board.uri) %} - {{ config.mod.link_merge }}  -{% endif %} -{% if mod|hasPermission(config.mod.cycle, board.uri) %} - {% if post.cycle %} - {{ config.mod.link_uncycle }}  - {% else %} - {{ config.mod.link_cycle }}  + {% if mod|hasPermission(config.mod.merge, board.uri) %} + {{ config.mod.link_merge }}  + {% endif %} + {% if mod|hasPermission(config.mod.cycle, board.uri) %} + {% if post.cycle %} + {{ config.mod.link_uncycle }}  + {% else %} + {{ config.mod.link_cycle }}  + {% endif %} + {% endif %} + {% if mod|hasPermission(config.mod.bumplock, board.uri) %} + {% if post.sage %} + {{ config.mod.link_bumpunlock }}  + {% else %} + {{ config.mod.link_bumplock }}  + {% endif %} + {% endif %} + {% if mod|hasPermission(config.mod.lock, board.uri) %} + {% if post.locked %} + {{ config.mod.link_unlock }}  + {% else %} + {{ config.mod.link_lock }}  + {% endif %} + {% endif %} + + {% endif %} + {% if mod|hasPermission(config.mod.move, board.uri) %} + {% if not post.thread %} + {{ config.mod.link_move }}  + {% else %} + {{ config.mod.link_move }}  + {% endif %} + {% endif %} + {% if mod|hasPermission(config.mod.editpost, board.uri) %} + {{ config.mod.link_editpost }}  + {% endif %} +{% else %} + {% if mod|hasPermission(config.mod.restore_shadow_post, board.uri) %} + {{ config.mod.link_sd_restore }}  + {% endif %} + {% if mod|hasPermission(config.mod.delete_shadow_post, board.uri) %} + {{ config.mod.link_sd_delete }}  {% endif %} -{% endif %} -{% if mod|hasPermission(config.mod.editpost, board.uri) %} - {{ config.mod.link_editpost }}  {% endif %} 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 #} -
    +

    {% if not index %}{% endif %}