diff --git a/inc/spbc-admin.php b/inc/spbc-admin.php index 9cbb9e3c1..3adbd8479 100644 --- a/inc/spbc-admin.php +++ b/inc/spbc-admin.php @@ -25,6 +25,7 @@ use CleantalkSP\SpbctWP\FileEditorDisabler\FileEditorDisabler; use CleantalkSP\SpbctWP\UsersPassCheckModule\UsersPassCheckHandler; use CleantalkSP\SpbctWP\Scanner\ScannerAjaxEndpoints; +use CleantalkSP\SpbctWP\Scanner\ScannerActions\BackupsActions; // Prevent direct call if ( ! defined('ABSPATH') ) { @@ -131,8 +132,8 @@ function spbc_admin_init() add_action('wp_ajax_spbc_check_file_block', array(\CleantalkSP\SpbctWP\Firewall\UploadChecker::class, 'uploadCheckerGetLastBlockInfo')); // Backups - add_action('wp_ajax_spbc_rollback', 'spbc_rollback'); - add_action('wp_ajax_spbc_backup__delete', 'spbc_backup__delete'); + add_action('wp_ajax_spbc_rollback', [BackupsActions::class, 'rollbackBackupAjax']); + add_action('wp_ajax_spbc_backup__delete', [BackupsActions::class, 'deleteBackupAjax']); // Misc add_action('wp_ajax_spbc_settings__get_description', 'spbc_settings__get_description'); diff --git a/inc/spbc-backups.php b/inc/spbc-backups.php deleted file mode 100644 index 78a4e84ca..000000000 --- a/inc/spbc-backups.php +++ /dev/null @@ -1,433 +0,0 @@ - true)) -{ - global $wpdb; - $result = $wpdb->get_row('SELECT COUNT(*) as cnt FROM ' . SPBC_TBL_BACKUPS . ' WHERE type = ' . Helper::prepareParamForSQLQuery(strtoupper($type)), OBJECT); - if ($result->cnt > 10) { - // suppress because data is already prepared in Helper::prepareParamForSQLQuery method - // @psalm-suppress WpdbUnsafeMethodsIssue - $result = $wpdb->get_results( - 'SELECT backup_id' - . ' FROM ' . SPBC_TBL_BACKUPS - . ' WHERE datetime < (' - . 'SELECT datetime' - . ' FROM ' . SPBC_TBL_BACKUPS - . ' WHERE type = ' . Helper::prepareParamForSQLQuery(strtoupper($type)) - . ' ORDER BY datetime DESC' - . ' LIMIT 9,1)' - ); - if ($result && count($result)) { - foreach ($result as $backup) { - $result = spbc_backup__delete(true, $backup->backup_id); - if ( ! empty($result['error'])) { - $out = array('error' => 'BACKUP_DELETE: ' . substr($result['error'], 0, 1024)); - } - } - } - } - - return $out; -} - -/** - * Delete backup log and file by real path - * @param string $real_path - * @return void - */ -function spbc_backup__delete_log_and_file($real_path) -{ - global $wpdb; - - $backup_record = spbc_backup__get_backup_by_real_path($real_path); - - if (!empty($backup_record)) { - spbc_backup__delete_file_log($real_path); - } - - // check if backup set is empty, then delete backup record, else remove only file - $sql = $wpdb->prepare('SELECT COUNT(*) FROM ' . SPBC_TBL_BACKUPED_FILES . ' WHERE backup_id = %d', $backup_record['backup_id']); - $backup_set = $wpdb->get_var($sql); - if ($backup_set == 0) { - spbc_backup__delete(true, $backup_record['backup_id']); - } else { - spbc_backup__delete_file($backup_record['back_path']); - } -} - -/** - * Delete backup file by backup path - * @param string $backup_file_path - * @return void - */ -function spbc_backup__delete_file($backup_file_path) -{ - if (file_exists($backup_file_path)) { - unlink($backup_file_path); - } -} - -/** - * Delete backup log and file by real path - * @param string $real_path - * @return void - */ -function spbc_backup__delete_file_log($real_path) -{ - global $wpdb; - - $wpdb->delete(SPBC_TBL_BACKUPED_FILES, array('real_path' => $real_path)); -} - -/** - * Get backup record by real path - * @param string $real_path - * @return array - */ -function spbc_backup__get_backup_by_real_path($real_path) -{ - global $wpdb; - - $sql = $wpdb->prepare( - 'SELECT * FROM ' . SPBC_TBL_BACKUPED_FILES . ' WHERE real_path = %s', - $real_path - ); - - $result = $wpdb->get_row($sql, ARRAY_A); - - if (!is_array($result)) { - return array(); - } - - return $result; -} - -/** - * Delete backup - * @param bool $direct_call - * @param int|null $backup_id - * @return array - */ -function spbc_backup__delete($direct_call = false, $backup_id = null) -{ - global $wpdb; - - if (!$direct_call) { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); - } - $backup_id = !$direct_call && !empty($_POST['backup_id']) ? (int)$_POST['backup_id'] : $backup_id; - - if (is_dir(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id)) { - // Deleting backup files - foreach (glob(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id . '/*') as $filename) { - if (!unlink($filename)) { - $output = array('error' => 'FILE_DELETE_ERROR: ' . substr($filename, 0, 1024)); - break; - } - } - - if (empty($output['error'])) { - if (rmdir(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id)) { - if (false !== $wpdb->delete(SPBC_TBL_BACKUPED_FILES, array('backup_id' => $backup_id), array('%d'))) { - if (false !== $wpdb->delete(SPBC_TBL_BACKUPS, array('backup_id' => $backup_id), array('%d'))) { - $output = array( - 'html' => 'Backup deleted', - 'success' => true, - 'color' => 'black', - 'background' => 'rgba(240, 110, 110, 0.7)', - ); - } else { - $output = array('error' => 'DELETING_BACKUP_DB_ERROR: ' . substr($wpdb->last_error, 0, 1024)); - } - } else { - $output = array('error' => 'DELETING_BACKUP_FILES_DB_ERROR: ' . substr($wpdb->last_error, 0, 1024)); - } - } else { - $output = array('error' => 'DIRECTORY_DELETE_ERROR: ' . substr(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id, 0, 1024)); - } - } - } else { - $output = array('comment' => 'DIRECTORY_NOT_EXISTS: ' . substr(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id, 0, 1024)); - } - - if (!$direct_call) { - wp_send_json($output); - } - - return $output; -} - -/** - * Make backup of files with signatures handler - * @return array - */ -function spbc_backup__files_with_signatures_handler() -{ - global $wpdb, $spbc; - - $output = array('success' => true); - - $files_to_backup = $wpdb->get_results('SELECT path, weak_spots, checked_heuristic, checked_signatures, status, severity FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE weak_spots LIKE "%\"SIGNATURES\":%";', ARRAY_A); - - if (!is_array($files_to_backup) || !count($files_to_backup)) { - $output = array('success' => true); - return $output; - } - - $sql_data = array(); - foreach ($files_to_backup as $file) { - if (spbc_file_has_backup($file['path'])) { - continue; - } - $weak_spots = json_decode($file['weak_spots'], true); - - $signtures_in_file = array(); - if (!empty($weak_spots['SIGNATURES'])) { - foreach ($weak_spots['SIGNATURES'] as $signatures_in_string) { - $signtures_in_file = array_merge($signtures_in_file, array_diff($signatures_in_string, $signtures_in_file)); - } - } - - if (empty($signtures_in_file)) { - continue; - } - - // Adding new backup batch - if ( ! isset($backup_id)) { - $wpdb->insert(SPBC_TBL_BACKUPS, array('type' => 'SIGNATURES', 'datetime' => date('Y-m-d H:i:s'))); - $backup_id = $wpdb->insert_id; - $spbc->data['scanner']['last_backup'] = $backup_id; - $spbc->save('data'); - $dir_name = SPBC_PLUGIN_DIR . 'backups/'; - if ( ! is_dir($dir_name)) { - mkdir($dir_name); - file_put_contents($dir_name . 'index.php', 'update(SPBC_TBL_BACKUPS, array('status' => 'STOPPED'), array('backup_id' => $backup_id)); - $output = $result; - break; - } - } - - if (empty($sql_data) || isset($output['error'])) { - $output = array('success' => true); - return $output; - } - - $backup_id = isset($backup_id) ? $backup_id : $spbc->data['scanner']['last_backup']; - - // Writing backuped files to DB - $sql_query = 'INSERT INTO ' . SPBC_TBL_BACKUPED_FILES . ' (backup_id, real_path, back_path, backup_prev_results_state) VALUES'; - // suppress because data is already prepared in Helper::prepareParamForSQLQuery method - // @psalm-suppress WpdbUnsafeMethodsIssue - $result = $wpdb->query($sql_query . implode(',', $sql_data) . ';'); - if ($result === false) { - $wpdb->update(SPBC_TBL_BACKUPS, array('status' => 'STOPPED'), array('backup_id' => $backup_id)); - $output = array('error' => 'DB_WRITE_ERROR: ' . substr($wpdb->last_error, 0, 1024)); - return $output; - } - - // Updating current backup status - $result = $wpdb->update(SPBC_TBL_BACKUPS, array('status' => 'BACKUPED'), array('backup_id' => $backup_id)); - if ($result === false) { - $output = array('error' => 'DB_WRITE_ERROR: ' . substr($wpdb->last_error, 0, 1024)); - return $output; - } - - $result = spbc_backup__rotate('signatures'); - if (!empty($result['error'])) { - $output = array('error' => 'BACKUP_ROTATE: ' . substr($result['error'], 0, 1024)); - return $output; - } - - $output = array('success' => true); - - return $output; -} - -/** - * Make backup of files with signatures - * @return array - */ -function spbc_backup__files_with_signatures($direct_call = false) -{ - if ( ! $direct_call) { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); - } - - $output = spbc_backup__files_with_signatures_handler(); - - $output['end'] = 1; - - if (!$direct_call) { - wp_send_json($output); - } - - return $output; -} - -function spbc_backup__file($filename, $backup_id) -{ - global $spbc; - - $file_path = spbc_get_root_path() . $filename; - - if (file_exists($file_path)) { - if (is_readable($file_path)) { - $backup_path = '/wp-content/plugins/security-malware-firewall/backups/backup_' - . $backup_id - . '/' . str_replace('/', '__', str_replace('\\', '__', $filename)) - . '.' . hash('sha256', $filename . $spbc->data['salt']); - - if (copy($file_path, spbc_get_root_path() . $backup_path)) { - $output = $backup_path; - } else { - $output = array('error' => 'COPY_FAILED'); - } - } else { - $output = array('error' => 'FILE_NOT_READABLE'); - } - } else { - $output = array('error' => 'FILE_NOT_EXISTS'); - } - - return $output; -} - -function spbc_rollback($direct_call = false, $backup_id = null) -{ - if ( ! $direct_call) { - spbc_check_ajax_referer('spbc_secret_nonce', 'security'); - } - - $backup_id = ! $direct_call && ! empty($_POST['backup_id']) ? (int) $_POST['backup_id'] : $backup_id; - - global $wpdb; - - $files_to_rollback = $wpdb->get_results('SELECT real_path, back_path FROM ' . SPBC_TBL_BACKUPED_FILES . ' WHERE backup_id = ' . $backup_id . ';', ARRAY_A); - - if (is_array($files_to_rollback) && count($files_to_rollback)) { - $wpdb->update(SPBC_TBL_BACKUPS, array('status' => 'ROLLBACK'), array('backup_id' => $backup_id)); - - foreach ($files_to_rollback as $file) { - $result = spbc_rollback__file($file['back_path'], $file['real_path']); - - if ( ! empty($result['error'])) { - $output = $result; - break; - } - } - - if (empty($output['error'])) { - if ($wpdb->delete(SPBC_TBL_BACKUPED_FILES, array('backup_id' => $backup_id), array('%d'))) { - if ($wpdb->delete(SPBC_TBL_BACKUPS, array('backup_id' => $backup_id), array('%d'))) { - rmdir(spbc_get_root_path() . '/wp-content/plugins/security-malware-firewall/backups/backup_' . $backup_id); - - $output = array( - 'html' => 'Rollback succeeded', - 'success' => true, - 'color' => 'black', - 'background' => 'rgba(110, 240, 110, 0.7)', - ); - } else { - $output = array('error' => 'DELETING_BACKUP_DB_WRITE_ERROR: ' . substr($wpdb->last_error, 0, 1024)); - } - } else { - $output = array('error' => 'DELETING_BACKUP_FILES_DB_WRITE_ERROR: ' . substr($wpdb->last_error, 0, 1024)); - } - } else { - $output = array('error' => 'FILE_BACKUP_ERROR: ' . $output['error'] . 'FILE: ' . $file['back_path']); - } - } else { - $output = array('error' => 'BACKUP_NOT_FOUND'); - } - - if (!$direct_call) { - wp_send_json($output); - } - - return $output; -} - -function spbc_rollback__file($back_path, $real_path) -{ - $back_path = spbc_get_root_path() . $back_path; - $real_path = spbc_get_root_path() . $real_path; - - if (file_exists($back_path)) { - if (is_writable($back_path)) { - if (is_dir(dirname($real_path))) { - if (copy($back_path, $real_path)) { - unlink($back_path); - - $output = array('success' => true); - } else { - $output = array('error' => 'COPY_FAILED'); - } - } else { - $output = array('error' => 'REAL_FILE_DIR_NOT_EXISTS'); - } - } else { - $output = array('error' => 'BACKUPED_FILE_NOT_WRITABLE'); - } - } else { - $output = array('error' => 'BACKUPED_FILE_NOT_EXISTS'); - } - - return $output; -} - -function spbc_backups_count_found() -{ - global $wpdb; - - $count = $wpdb->get_results( - 'SELECT COUNT(*) FROM ' . SPBC_TBL_BACKUPS, - OBJECT_K - ); - - return $count ? key($count) : 0; -} - -function spbc_file_has_backup($real_path) -{ - global $spbc, $wpdb; - $real_path = $spbc->is_windows ? str_replace('/', '\\', $real_path) : $real_path; - $query = 'SELECT * FROM ' . SPBC_TBL_BACKUPED_FILES; - $result = $wpdb->get_results($query, ARRAY_A); - foreach ($result as $row) { - if ($row['real_path'] === $real_path) { - return true; - } - } - return false; -} diff --git a/inc/spbc-settings.php b/inc/spbc-settings.php index 7e745caf5..67dc05b90 100644 --- a/inc/spbc-settings.php +++ b/inc/spbc-settings.php @@ -18,6 +18,7 @@ use CleantalkSP\SpbctWP\Scanner\OSCron\Storages\OsCronTasksStorage; use CleantalkSP\SpbctWP\Scanner\OSCron\View\OSCronView; use CleantalkSP\SpbctWP\Scanner\ScannerActions\LinksActions; +use CleantalkSP\SpbctWP\Scanner\ScannerActions\BackupsActions; use CleantalkSP\SpbctWP\Scanner\ScannerActions\ScanResultsTableActions; use CleantalkSP\SpbctWP\Scanner\ScanningLog\ScanningLogFacade; use CleantalkSP\SpbctWP\Variables\Cookie; @@ -3455,7 +3456,7 @@ function spbc_list_table__get_args_by_type($table_type) 'get_array' => false, 'where' => ' RIGHT JOIN ' . SPBC_TBL_BACKUPED_FILES . ' ON ' . SPBC_TBL_BACKUPS . '.backup_id = ' . SPBC_TBL_BACKUPED_FILES . '.backup_id', ), - 'func_data_total' => 'spbc_backups_count_found', + 'func_data_total' => [BackupsActions::class, 'countBackups'], 'func_data_get' => 'spbc_field_backups__get_data', 'if_empty_items' => '

' . __('No backups found', 'security-malware-firewall') . '

', 'columns' => array( diff --git a/lib/CleantalkSP/SpbctWP/RemoteCalls.php b/lib/CleantalkSP/SpbctWP/RemoteCalls.php index c3857787d..43932bb70 100644 --- a/lib/CleantalkSP/SpbctWP/RemoteCalls.php +++ b/lib/CleantalkSP/SpbctWP/RemoteCalls.php @@ -3,6 +3,7 @@ namespace CleantalkSP\SpbctWP; use CleantalkSP\SpbctWP\Cron as SpbcCron; +use CleantalkSP\SpbctWP\Scanner\ScannerActions\BackupsActions; use CleantalkSP\SpbctWP\Scanner\ScannerAjaxEndpoints; use CleantalkSP\SpbctWP\Scanner\ScanningLog\Repository; use CleantalkSP\Variables\Get; @@ -281,7 +282,7 @@ public static function action__cron_update_task() // phpcs:ignore PSR1.Methods.C */ public static function action__rollback_repair() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps { - $result = spbc_rollback(Request::getString('backup_id')); + $result = BackupsActions::rollbackBackupAjax(true, Request::getString('backup_id')); die(empty($result['error']) ? 'OK' : 'FAIL ' . json_encode(array('error' => $result['error']))); diff --git a/lib/CleantalkSP/SpbctWP/Scanner/CureLog/CureLog.php b/lib/CleantalkSP/SpbctWP/Scanner/CureLog/CureLog.php index 8d94c815e..ab957644f 100644 --- a/lib/CleantalkSP/SpbctWP/Scanner/CureLog/CureLog.php +++ b/lib/CleantalkSP/SpbctWP/Scanner/CureLog/CureLog.php @@ -3,6 +3,7 @@ namespace CleantalkSP\SpbctWP\Scanner\CureLog; use CleantalkSP\SpbctWP\DB; +use CleantalkSP\SpbctWP\Scanner\ScannerActions\BackupsActions; class CureLog { @@ -181,7 +182,7 @@ public function getDataToPDF() public function logCureResult(CureLogRecord $cure_log_record) { - if (spbc_file_has_backup($cure_log_record->real_path)) { + if (BackupsActions::fileHasBackup($cure_log_record->real_path)) { $cure_log_record->has_backup = 1; } @@ -357,7 +358,7 @@ public function deleteCureLogAndBackupRecords($fast_hash) // if file has backup, delete backup record and backup file if (isset($cure_record['has_backup']) && $cure_record['has_backup'] == 1) { - spbc_backup__delete_log_and_file($cure_record['real_path']); + BackupsActions::deleteLogAndFile($cure_record['real_path']); } } diff --git a/lib/CleantalkSP/SpbctWP/Scanner/ScannerActions/BackupsActions.php b/lib/CleantalkSP/SpbctWP/Scanner/ScannerActions/BackupsActions.php index 355a56042..ca5b389ff 100644 --- a/lib/CleantalkSP/SpbctWP/Scanner/ScannerActions/BackupsActions.php +++ b/lib/CleantalkSP/SpbctWP/Scanner/ScannerActions/BackupsActions.php @@ -3,9 +3,380 @@ namespace CleantalkSP\SpbctWP\Scanner\ScannerActions; use CleantalkSP\SpbctWP\Scanner\Cure; +use CleantalkSP\SpbctWP\Helpers\Helper; class BackupsActions { + // ==================== AJAX HANDLERS ==================== + + /** + * AJAX handler for deleting backup + * @param bool $direct_call + * @param int|null $backup_id + * @return array + * @psalm-suppress PossiblyUnusedReturnValue + */ + public static function deleteBackupAjax($direct_call = false, $backup_id = null) + { + if (!$direct_call) { + spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + } + $backup_id = !$direct_call && !empty($_POST['backup_id']) ? (int)$_POST['backup_id'] : $backup_id; + + $output = self::deleteBackupById($backup_id); + $output = self::prepareAjaxResponse($output, 'Backup deleted', 'rgba(240, 110, 110, 0.7)'); + + if (!$direct_call) { + wp_send_json($output); + } + + return $output; + } + + /** + * AJAX handler for rollback backup + * @param bool $direct_call + * @param int|string|null $backup_id + * @return array + */ + public static function rollbackBackupAjax($direct_call = false, $backup_id = null) + { + if (!$direct_call) { + spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + } + $backup_id = !$direct_call && !empty($_POST['backup_id']) ? (int)$_POST['backup_id'] : $backup_id; + + $output = self::rollbackBackup($backup_id); + $output = self::prepareAjaxResponse($output, 'Rollback succeeded', 'rgba(110, 240, 110, 0.7)'); + + if (!$direct_call) { + wp_send_json($output); + } + + return $output; + } + + /** + * AJAX handler for creating backups of files with signatures + * @param bool $direct_call + * @return array + */ + public static function createBackupsForFilesWithSignaturesAjax($direct_call = false) + { + if (!$direct_call) { + spbc_check_ajax_referer('spbc_secret_nonce', 'security'); + } + + $output = self::createBackupsForFilesWithSignatures(); + $output['end'] = 1; + + if (!$direct_call) { + wp_send_json($output); + } + + return $output; + } + + /** + * Helper to prepare AJAX response with HTML formatting + * @param array $output + * @param string $message + * @param string $background + * @return array + */ + private static function prepareAjaxResponse($output, $message, $background) + { + if (!empty($output['success'])) { + $output['html'] = '' . $message . ''; + $output['color'] = 'black'; + $output['background'] = $background; + } + return $output; + } + + // ==================== BUSINESS LOGIC ==================== + /** + * Check if file has backup + * @param string $real_path + * @return bool + */ + public static function fileHasBackup($real_path) + { + global $spbc, $wpdb; + $real_path = $spbc->is_windows ? str_replace('/', '\\', $real_path) : $real_path; + $query = 'SELECT * FROM ' . SPBC_TBL_BACKUPED_FILES; + $result = $wpdb->get_results($query, ARRAY_A); + foreach ($result as $row) { + if ($row['real_path'] === $real_path) { + return true; + } + } + return false; + } + + /** + * Get backup record by real path + * @param string $real_path + * @return array + */ + public static function getBackupByRealPath($real_path) + { + global $wpdb; + + $sql = $wpdb->prepare( + 'SELECT * FROM ' . SPBC_TBL_BACKUPED_FILES . ' WHERE real_path = %s', + $real_path + ); + + $result = $wpdb->get_row($sql, ARRAY_A); + + if (!is_array($result)) { + return array(); + } + + return $result; + } + + /** + * Delete backup log and file by real path + * @param string $real_path + * @return void + */ + public static function deleteLogAndFile($real_path) + { + global $wpdb; + + $backup_record = self::getBackupByRealPath($real_path); + + if (!empty($backup_record)) { + self::removeBackupFromDB($real_path); + } + + // check if backup set is empty, then delete backup record, else remove only file + if (!empty($backup_record['backup_id'])) { + $sql = $wpdb->prepare('SELECT COUNT(*) FROM ' . SPBC_TBL_BACKUPED_FILES . ' WHERE backup_id = %d', $backup_record['backup_id']); + $backup_set = $wpdb->get_var($sql); + if ($backup_set == 0) { + self::deleteBackupById($backup_record['backup_id']); + } elseif (!empty($backup_record['back_path'])) { + self::deleteBackupFile($backup_record['back_path']); + } + } + } + + /** + * Delete backup by ID (files and DB records) + * @param int $backup_id + * @return array + */ + public static function deleteBackupById($backup_id) + { + global $wpdb; + + $output = array(); + + if (is_dir(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id)) { + // Deleting backup files + foreach (glob(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id . '/*') as $filename) { + if (!unlink($filename)) { + $output = array('error' => 'FILE_DELETE_ERROR: ' . substr($filename, 0, 1024)); + break; + } + } + + if (empty($output['error'])) { + if (rmdir(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id)) { + if (false !== $wpdb->delete(SPBC_TBL_BACKUPED_FILES, array('backup_id' => $backup_id), array('%d'))) { + if (false !== $wpdb->delete(SPBC_TBL_BACKUPS, array('backup_id' => $backup_id), array('%d'))) { + $output = array('success' => true); + } else { + $output = array('error' => 'DELETING_BACKUP_DB_ERROR: ' . substr($wpdb->last_error, 0, 1024)); + } + } else { + $output = array('error' => 'DELETING_BACKUP_FILES_DB_ERROR: ' . substr($wpdb->last_error, 0, 1024)); + } + } else { + $output = array('error' => 'DIRECTORY_DELETE_ERROR: ' . substr(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id, 0, 1024)); + } + } + } else { + $output = array('comment' => 'DIRECTORY_NOT_EXISTS: ' . substr(SPBC_PLUGIN_DIR . 'backups/backup_' . $backup_id, 0, 1024)); + } + + return $output; + } + + /** + * Create backups for files with signatures + * @return array + */ + public static function createBackupsForFilesWithSignatures() + { + global $wpdb, $spbc; + + $output = array('success' => true); + + $files_to_backup = $wpdb->get_results('SELECT path, weak_spots, checked_heuristic, checked_signatures, status, severity FROM ' . SPBC_TBL_SCAN_FILES . ' WHERE weak_spots LIKE "%\"SIGNATURES\":%";', ARRAY_A); + + if (!is_array($files_to_backup) || !count($files_to_backup)) { + return $output; + } + + $sql_data = array(); + $backup_id = null; + + foreach ($files_to_backup as $file) { + if (self::fileHasBackup($file['path'])) { + continue; + } + $weak_spots = json_decode($file['weak_spots'], true); + + $signtures_in_file = array(); + if (!empty($weak_spots['SIGNATURES'])) { + foreach ($weak_spots['SIGNATURES'] as $signatures_in_string) { + $signtures_in_file = array_merge($signtures_in_file, array_diff($signatures_in_string, $signtures_in_file)); + } + } + + if (empty($signtures_in_file)) { + continue; + } + + // Adding new backup batch + if (!isset($backup_id)) { + $wpdb->insert(SPBC_TBL_BACKUPS, array('type' => 'SIGNATURES', 'datetime' => date('Y-m-d H:i:s'))); + $backup_id = $wpdb->insert_id; + $spbc->data['scanner']['last_backup'] = $backup_id; + $spbc->save('data'); + $dir_name = SPBC_PLUGIN_DIR . 'backups/'; + if (!is_dir($dir_name)) { + mkdir($dir_name); + file_put_contents($dir_name . 'index.php', 'update(SPBC_TBL_BACKUPS, array('status' => 'STOPPED'), array('backup_id' => $backup_id)); + $output = $result; + break; + } + } + + if (empty($sql_data) || isset($output['error'])) { + return array('success' => true); + } + + $backup_id = isset($backup_id) ? $backup_id : $spbc->data['scanner']['last_backup']; + + // Writing backuped files to DB + $sql_query = 'INSERT INTO ' . SPBC_TBL_BACKUPED_FILES . ' (backup_id, real_path, back_path, backup_prev_results_state) VALUES'; + // @psalm-suppress WpdbUnsafeMethodsIssue + $result = $wpdb->query($sql_query . implode(',', $sql_data) . ';'); + if ($result === false) { + $wpdb->update(SPBC_TBL_BACKUPS, array('status' => 'STOPPED'), array('backup_id' => $backup_id)); + return array('error' => 'DB_WRITE_ERROR: ' . substr($wpdb->last_error, 0, 1024)); + } + + // Updating current backup status + $result = $wpdb->update(SPBC_TBL_BACKUPS, array('status' => 'BACKUPED'), array('backup_id' => $backup_id)); + if ($result === false) { + return array('error' => 'DB_WRITE_ERROR: ' . substr($wpdb->last_error, 0, 1024)); + } + + $result = self::rotateBackups('signatures'); + if (!empty($result['error'])) { + return array('error' => 'BACKUP_ROTATE: ' . substr($result['error'], 0, 1024)); + } + + return array('success' => true); + } + + /** + * Backup a single file + * @param string $filename + * @param int $backup_id + * @return string|array Backup path on success, error array on failure + */ + public static function backupFile($filename, $backup_id) + { + global $spbc; + + $file_path = spbc_get_root_path() . $filename; + + if (file_exists($file_path)) { + if (is_readable($file_path)) { + $backup_path = '/wp-content/plugins/security-malware-firewall/backups/backup_' + . $backup_id + . '/' . str_replace('/', '__', str_replace('\\', '__', $filename)) + . '.' . hash('sha256', $filename . $spbc->data['salt']); + + if (copy($file_path, spbc_get_root_path() . $backup_path)) { + return $backup_path; + } else { + return array('error' => 'COPY_FAILED'); + } + } else { + return array('error' => 'FILE_NOT_READABLE'); + } + } else { + return array('error' => 'FILE_NOT_EXISTS'); + } + } + + /** + * Rotate backups (keep last 10) + * @param string $type + * @param array $out + * @return array + */ + public static function rotateBackups($type = 'signatures', $out = array('success' => true)) + { + global $wpdb; + $result = $wpdb->get_row('SELECT COUNT(*) as cnt FROM ' . SPBC_TBL_BACKUPS . ' WHERE type = ' . Helper::prepareParamForSQLQuery(strtoupper($type)), OBJECT); + if ($result->cnt > 10) { + // @psalm-suppress WpdbUnsafeMethodsIssue + $result = $wpdb->get_results( + 'SELECT backup_id' + . ' FROM ' . SPBC_TBL_BACKUPS + . ' WHERE datetime < (' + . 'SELECT datetime' + . ' FROM ' . SPBC_TBL_BACKUPS + . ' WHERE type = ' . Helper::prepareParamForSQLQuery(strtoupper($type)) + . ' ORDER BY datetime DESC' + . ' LIMIT 9,1)' + ); + if ($result && count($result)) { + foreach ($result as $backup) { + $result = self::deleteBackupById($backup->backup_id); + if (!empty($result['error'])) { + $out = array('error' => 'BACKUP_DELETE: ' . substr($result['error'], 0, 1024)); + } + } + } + } + + return $out; + } + /** * tested * Restore file from backup handler @@ -150,4 +521,90 @@ public static function removeBackupFromDB($file_path) return array(); } + + /** + * Rollback entire backup by ID + * @param int|string $backup_id + * @return array + */ + public static function rollbackBackup($backup_id) + { + global $wpdb; + + $files_to_rollback = $wpdb->get_results('SELECT real_path, back_path FROM ' . SPBC_TBL_BACKUPED_FILES . ' WHERE backup_id = ' . (int)$backup_id . ';', ARRAY_A); + + if (!is_array($files_to_rollback) || !count($files_to_rollback)) { + return array('error' => 'BACKUP_NOT_FOUND'); + } + + $wpdb->update(SPBC_TBL_BACKUPS, array('status' => 'ROLLBACK'), array('backup_id' => $backup_id)); + + foreach ($files_to_rollback as $file) { + $result = self::rollbackFile($file['back_path'], $file['real_path']); + + if (!empty($result['error'])) { + return array('error' => 'FILE_BACKUP_ERROR: ' . $result['error'] . 'FILE: ' . $file['back_path']); + } + } + + if ($wpdb->delete(SPBC_TBL_BACKUPED_FILES, array('backup_id' => $backup_id), array('%d'))) { + if ($wpdb->delete(SPBC_TBL_BACKUPS, array('backup_id' => $backup_id), array('%d'))) { + rmdir(spbc_get_root_path() . '/wp-content/plugins/security-malware-firewall/backups/backup_' . $backup_id); + return array('success' => true); + } else { + return array('error' => 'DELETING_BACKUP_DB_WRITE_ERROR: ' . substr($wpdb->last_error, 0, 1024)); + } + } else { + return array('error' => 'DELETING_BACKUP_FILES_DB_WRITE_ERROR: ' . substr($wpdb->last_error, 0, 1024)); + } + } + + /** + * Rollback a single file from backup + * @param string $back_path Relative backup path + * @param string $real_path Relative real file path + * @return array + */ + public static function rollbackFile($back_path, $real_path) + { + $back_path = spbc_get_root_path() . $back_path; + $real_path = spbc_get_root_path() . $real_path; + + if (!file_exists($back_path)) { + return array('error' => 'BACKUPED_FILE_NOT_EXISTS'); + } + + if (!is_writable($back_path)) { + return array('error' => 'BACKUPED_FILE_NOT_WRITABLE'); + } + + if (!is_dir(dirname($real_path))) { + return array('error' => 'REAL_FILE_DIR_NOT_EXISTS'); + } + + if (!copy($back_path, $real_path)) { + return array('error' => 'COPY_FAILED'); + } + + unlink($back_path); + + return array('success' => true); + } + + /** + * Count total backups + * @return int + * @psalm-suppress PossiblyUnusedMethod + */ + public static function countBackups() + { + global $wpdb; + + $count = $wpdb->get_results( + 'SELECT COUNT(*) FROM ' . SPBC_TBL_BACKUPS, + OBJECT_K + ); + + return $count ? (int)key($count) : 0; + } } diff --git a/lib/CleantalkSP/SpbctWP/Scanner/ScannerQueue.php b/lib/CleantalkSP/SpbctWP/Scanner/ScannerQueue.php index c24c291c2..9f1a24068 100644 --- a/lib/CleantalkSP/SpbctWP/Scanner/ScannerQueue.php +++ b/lib/CleantalkSP/SpbctWP/Scanner/ScannerQueue.php @@ -13,6 +13,7 @@ use CleantalkSP\SpbctWP\Scanner\DBTrigger\DBTriggerModel; use CleantalkSP\SpbctWP\Scanner\OSCron\OSCronModel; use CleantalkSP\SpbctWP\Scanner\ScannerActions\FileSystemActions; +use CleantalkSP\SpbctWP\Scanner\ScannerActions\BackupsActions; use CleantalkSP\SpbctWP\Scanner\ScannerActions\LinksActions; use CleantalkSP\SpbctWP\Scanner\ScannerActions\ScanResultsTableActions; use CleantalkSP\SpbctWP\Scanner\ScannerInteractivity\ScannerInteractivityData; @@ -1877,7 +1878,7 @@ public function schedule_send_heuristic_suspicious_files() // phpcs:ignore PSR1. public function auto_cure_backup() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps { - return spbc_backup__files_with_signatures(true); + return BackupsActions::createBackupsForFilesWithSignaturesAjax(true); } /** diff --git a/lib/CleantalkSP/SpbctWP/Scanner/Stages/CureStage.php b/lib/CleantalkSP/SpbctWP/Scanner/Stages/CureStage.php index a86b5e545..8ab6c6b49 100644 --- a/lib/CleantalkSP/SpbctWP/Scanner/Stages/CureStage.php +++ b/lib/CleantalkSP/SpbctWP/Scanner/Stages/CureStage.php @@ -9,6 +9,7 @@ use CleantalkSP\SpbctWP\Scanner\CureLog\CureLog; use CleantalkSP\SpbctWP\Scanner\CureLog\CureLogRecord; use CleantalkSP\SpbctWP\Scanner\FileInfoExtended; +use CleantalkSP\SpbctWP\Scanner\ScannerActions\BackupsActions; use CleantalkSP\SpbctWP\Scanner\ScanningLog\ScanningLogFacade; use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\ScanningStagesStorage; use CleantalkSP\SpbctWP\Scanner\ScanningStagesModule\Stages\AutoCure; @@ -231,7 +232,7 @@ public function processCure($file) */ private function preCheckFile($file, $cure_log_record) { - spbc_backup__files_with_signatures(true); + BackupsActions::createBackupsForFilesWithSignaturesAjax(true); //check if file has backup if ( !$this->fileHasBackup($file) ) { @@ -255,7 +256,7 @@ private function preCheckFile($file, $cure_log_record) */ private function fileHasBackup($file) { - return spbc_file_has_backup($file['path']); + return BackupsActions::fileHasBackup($file['path']); } /** diff --git a/security-malware-firewall.php b/security-malware-firewall.php index 3a546b3ab..d03ea7ef4 100644 --- a/security-malware-firewall.php +++ b/security-malware-firewall.php @@ -151,7 +151,6 @@ require_once SPBC_PLUGIN_DIR . 'lib/spbc-php-patch.php'; // PHP functions patches require_once SPBC_PLUGIN_DIR . 'lib/autoloader.php'; // Autoloader -require_once SPBC_PLUGIN_DIR . 'inc/spbc-backups.php'; require_once SPBC_PLUGIN_DIR . 'inc/fw-update.php'; // Misc libs