diff --git a/README.md b/README.md new file mode 100644 index 0000000..f5fd40a --- /dev/null +++ b/README.md @@ -0,0 +1,103 @@ +# WP Security Pack + +WordPress security without the bullshit. No upsells, no premium tier, no fake threat counters. + +## Requirements + +- WordPress 5.0+ +- PHP 7.4+ + +## What It Does + +### Login Protection +- Blocks IPs after failed login attempts +- Custom login URL (hides wp-login.php) +- Hides wp-admin from logged-out users +- Honeypot field for bots +- Hides login errors (stops username enumeration) +- Email alerts for admin logins from new IPs +- Country/IP restrictions on login page + +### IP Control +- Whitelist and blacklist +- Auto-blacklist after repeated lockouts +- IPv4, IPv6, CIDR supported + +### Geo Blocking +- Block countries +- Uses free IP2Location LITE database +- One-click download + +### Hardening +- Disable XML-RPC +- Disable dashboard file editing +- Disable application passwords +- Restrict REST API to logged-in users +- Remove WordPress version +- Block user enumeration (?author=1 and REST API) +- Disable pingbacks/trackbacks + +### Security Headers +X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Referrer-Policy, Permissions-Policy, Content-Security-Policy, HSTS + +### Two-Factor Authentication +- TOTP (Google Authenticator, Authy, etc.) +- Backup codes +- Enforce for admins + +### File Integrity Monitoring +- Checks WordPress core files against official checksums +- Daily scans +- Email alerts on changes + +### Malware Scanner +- Scans plugins, themes, uploads +- Pattern-based detection +- Weekly scans +- Does not scan core files (that's what File Integrity is for) + +### Activity Log +- Login attempts, lockouts, blocks +- IP, country, username, timestamp +- Configurable retention +- CSV export + +### Tools +- Export/import settings +- Force logout all users +- Test email + +## Installation + +1. Upload to `/wp-content/plugins/wp-security-pack/` +2. Activate +3. Configure under Security menu + +## FAQ + +**Premium version?** +No. This is it. + +**Will it slow my site?** +No. Checks run on login and admin access, not frontend loads. + +**Locked myself out?** +Rename the plugin folder via FTP/SSH. Log in. Fix settings. + +**Geo-blocking without database?** +Won't work. Download IP2Location LITE from settings. It's free. + +**Use with other security plugins?** +Possible but likely conflicts. Pick one. + +## Privacy + +No tracking. No analytics. No telemetry. + +External connections: +- WordPress.org API (core file checksums) +- IP2Location (database download, only when you click it) + +## License + +GPLv2 or later diff --git a/README.txt b/README.txt deleted file mode 100644 index 6cb6213..0000000 --- a/README.txt +++ /dev/null @@ -1,145 +0,0 @@ -=== WP Security Pack === -Contributors: arkhost -Tags: security, login, firewall, 2fa, malware -Requires at least: 5.0 -Tested up to: 6.4 -Requires PHP: 7.4 -Stable tag: 1.0 -License: GPLv2 or later -License URI: https://www.gnu.org/licenses/gpl-2.0.html - -A security plugin. No upsells, no premium version, no "threat counters". Just security settings that work. - -== Description == - -WP Security Pack adds security features to WordPress. It's free, open source, and doesn't nag you to upgrade. - -**What it does:** - -= Login Protection = -* Limit login attempts (block IPs after failed logins) -* Custom login URL (hide wp-login.php) -* Hide wp-admin from non-logged-in users -* Honeypot field to catch bots -* Hide login error messages to prevent username enumeration -* Email alerts when admins log in from new IPs -* Restrict login page access by country or IP - -= IP Control = -* Whitelist IPs (always allowed) -* Blacklist IPs (always blocked) -* Auto-blacklist IPs after repeated lockouts -* Supports IPv4, IPv6, and CIDR notation - -= Geo Blocking = -* Block access from specific countries -* Uses free IP2Location LITE database -* One-click database download - -= Hardening = -* Disable XML-RPC -* Disable file editing in dashboard -* Disable application passwords -* Restrict REST API to logged-in users -* Remove WordPress version from source -* Disable user enumeration (?author=1 and REST API) -* Disable pingbacks and trackbacks - -= Security Headers = -* X-Content-Type-Options -* X-Frame-Options -* X-XSS-Protection -* Referrer-Policy -* Permissions-Policy -* Content-Security-Policy -* Strict-Transport-Security (HSTS) - -= Two-Factor Authentication = -* TOTP-based (works with Google Authenticator, Authy, etc.) -* Backup codes -* Option to enforce 2FA for administrators - -= File Integrity Monitoring = -* Monitors WordPress core files -* Compares against official WordPress.org checksums -* Daily automatic scans -* Email alerts when files change - -= Malware Scanner = -* Scans plugins, themes, and uploads -* Pattern-based detection for common threats -* Does not scan WordPress core (use File Integrity for that) -* Weekly automatic scans - -= Activity Log = -* Records login attempts (success and failed) -* Records lockouts and blocks -* Shows IP, country, username, timestamp -* Configurable retention period -* Export to CSV - -= Tools = -* Export/import settings -* Force logout all users -* Test email delivery - -**What it doesn't do:** - -* Firewall at the server level (use your host's firewall) -* Real-time threat intelligence (we don't phone home) -* Backup your site (use a backup plugin) -* CDN or performance optimization - -**Privacy:** - -The plugin doesn't send data anywhere except: - -* WordPress.org API to verify core file checksums -* IP2Location to download the geo database (if you click the download button) - -No tracking, no analytics, no telemetry. - -== Installation == - -1. Upload the plugin files to `/wp-content/plugins/wp-security-pack/` -2. Activate the plugin through the 'Plugins' screen -3. Go to Security in the admin menu to configure - -== Frequently Asked Questions == - -= Is there a premium version? = - -No. This is the complete plugin. - -= Will this slow down my site? = - -The plugin runs security checks on specific actions (login, admin access). It doesn't add overhead to frontend page loads for regular visitors. - -= I locked myself out = - -Connect to your server via FTP or SSH and rename the plugin folder. This disables the plugin and you can log in normally. - -= Does geo-blocking work without the database? = - -No. You need to download the IP2Location LITE database from the plugin settings. It's free. - -= Can I use this with other security plugins? = - -Probably, but there may be conflicts. We recommend using one security plugin at a time. - -== Screenshots == - -1. Status overview -2. Login protection settings -3. Activity log -4. Two-factor authentication setup - -== Changelog == - -= 1.0 = -* Initial release - -== Upgrade Notice == - -= 1.0 = -Initial release. diff --git a/wp-security-pack/includes/class-wpsp-admin.php b/wp-security-pack/includes/class-wpsp-admin.php index e667127..061f93d 100644 --- a/wp-security-pack/includes/class-wpsp-admin.php +++ b/wp-security-pack/includes/class-wpsp-admin.php @@ -25,6 +25,7 @@ class WPSP_Admin { add_action( 'wp_dashboard_setup', array( $this, 'add_dashboard_widget' ) ); add_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 10, 2 ); add_filter( 'plugin_action_links_' . WPSP_PLUGIN_BASENAME, array( $this, 'plugin_action_links' ) ); + add_filter( 'submenu_file', array( $this, 'highlight_submenu' ) ); // AJAX handlers. add_action( 'wp_ajax_wpsp_clear_logs', array( $this, 'ajax_clear_logs' ) ); @@ -41,6 +42,10 @@ class WPSP_Admin { add_action( 'wp_ajax_wpsp_reset_settings', array( $this, 'ajax_reset_settings' ) ); add_action( 'wp_ajax_wpsp_test_email', array( $this, 'ajax_test_email' ) ); add_action( 'wp_ajax_wpsp_force_logout_all', array( $this, 'ajax_force_logout_all' ) ); + add_action( 'wp_ajax_wpsp_quarantine_file', array( $this, 'ajax_quarantine_file' ) ); + add_action( 'wp_ajax_wpsp_restore_file', array( $this, 'ajax_restore_file' ) ); + add_action( 'wp_ajax_wpsp_delete_quarantined', array( $this, 'ajax_delete_quarantined' ) ); + add_action( 'wp_ajax_wpsp_delete_wp_file', array( $this, 'ajax_delete_wp_file' ) ); } /** @@ -83,6 +88,36 @@ class WPSP_Admin { } } + /** + * Highlight the correct submenu based on current tab. + * + * @param string $submenu_file The current submenu file. + * @return string + */ + public function highlight_submenu( $submenu_file ) { + global $pagenow; + + if ( 'admin.php' !== $pagenow ) { + return $submenu_file; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; + + if ( 'wp-security-pack' !== $page ) { + return $submenu_file; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $tab = isset( $_GET['tab'] ) ? sanitize_text_field( wp_unslash( $_GET['tab'] ) ) : 'status'; + + if ( 'status' === $tab ) { + return 'wp-security-pack'; + } + + return 'wp-security-pack&tab=' . $tab; + } + /** * Add plugin action links (left side - Settings, Deactivate, etc.). * @@ -565,8 +600,129 @@ class WPSP_Admin { + + + + + + + + + + + + + + + + prefix ) : ?> + wp_ + + + prefix ); ?> + + + + + + + + + + + + + + + + + + + + + + + + + +
+

+
+

+
+ + + + + + + + + + + + + + + + + + + + + + + + +
readme.html + +
license.txt + +
+
+ + - 0 ) : ?> + 0 ) : + ?> + + + + + @@ -1817,11 +1988,12 @@ class WPSP_Admin { + $findings ) : ?> - + @@ -1834,6 +2006,11 @@ class WPSP_Admin { + + + @@ -1852,6 +2029,41 @@ class WPSP_Admin {

+ + get_quarantined_files(); + if ( ! empty( $quarantined_files ) ) : + ?> +

+

+ + + + + + + + + + + $meta ) : ?> + + + + + + + + +
+ + +
+
@@ -1866,8 +2078,8 @@ class WPSP_Admin {

- - + + @@ -1875,11 +2087,6 @@ class WPSP_Admin { - - - - -
@@ -1933,6 +2140,86 @@ class WPSP_Admin { location.reload(); }); }); + + // Quarantine file. + $('.wpsp-quarantine-file').on('click', function() { + var $btn = $(this); + var filePath = $btn.data('file'); + + if (!confirm('')) { + return; + } + + $btn.prop('disabled', true).text(''); + + $.post(ajaxurl, { + action: 'wpsp_quarantine_file', + file_path: filePath, + _ajax_nonce: '' + }, function(response) { + if (response.success) { + $btn.closest('tr').fadeOut(function() { + $(this).remove(); + location.reload(); + }); + } else { + alert(response.data.message || ''); + $btn.prop('disabled', false).text(''); + } + }); + }); + + // Restore file from quarantine. + $('.wpsp-restore-file').on('click', function() { + var $btn = $(this); + var name = $btn.data('name'); + + if (!confirm('')) { + return; + } + + $btn.prop('disabled', true).text(''); + + $.post(ajaxurl, { + action: 'wpsp_restore_file', + quarantine_name: name, + _ajax_nonce: '' + }, function(response) { + if (response.success) { + location.reload(); + } else { + alert(response.data.message || ''); + $btn.prop('disabled', false).text(''); + } + }); + }); + + // Delete quarantined file permanently. + $('.wpsp-delete-quarantined').on('click', function() { + var $btn = $(this); + var name = $btn.data('name'); + + if (!confirm('')) { + return; + } + + $btn.prop('disabled', true).text(''); + + $.post(ajaxurl, { + action: 'wpsp_delete_quarantined', + quarantine_name: name, + _ajax_nonce: '' + }, function(response) { + if (response.success) { + $btn.closest('tr').fadeOut(function() { + $(this).remove(); + }); + } else { + alert(response.data.message || ''); + $btn.prop('disabled', false).text(''); + } + }); + }); }); __( 'Permission denied.', 'wp-security-pack' ) ) ); + } + + $file_path = isset( $_POST['file_path'] ) ? sanitize_text_field( wp_unslash( $_POST['file_path'] ) ) : ''; + + if ( empty( $file_path ) ) { + wp_send_json_error( array( 'message' => __( 'No file specified.', 'wp-security-pack' ) ) ); + } + + $scanner = new WPSP_Malware_Scanner(); + $result = $scanner->quarantine_file( $file_path ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => $result->get_error_message() ) ); + } + + wp_send_json_success( $result ); + } + + /** + * AJAX: Restore a file from quarantine. + */ + public function ajax_restore_file() { + check_ajax_referer( 'wpsp_admin' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-security-pack' ) ) ); + } + + $quarantine_name = isset( $_POST['quarantine_name'] ) ? sanitize_file_name( wp_unslash( $_POST['quarantine_name'] ) ) : ''; + + if ( empty( $quarantine_name ) ) { + wp_send_json_error( array( 'message' => __( 'No file specified.', 'wp-security-pack' ) ) ); + } + + $scanner = new WPSP_Malware_Scanner(); + $result = $scanner->restore_file( $quarantine_name ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => $result->get_error_message() ) ); + } + + wp_send_json_success( $result ); + } + + /** + * AJAX: Delete a quarantined file permanently. + */ + public function ajax_delete_quarantined() { + check_ajax_referer( 'wpsp_admin' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-security-pack' ) ) ); + } + + $quarantine_name = isset( $_POST['quarantine_name'] ) ? sanitize_file_name( wp_unslash( $_POST['quarantine_name'] ) ) : ''; + + if ( empty( $quarantine_name ) ) { + wp_send_json_error( array( 'message' => __( 'No file specified.', 'wp-security-pack' ) ) ); + } + + $scanner = new WPSP_Malware_Scanner(); + $result = $scanner->delete_quarantined_file( $quarantine_name ); + + if ( is_wp_error( $result ) ) { + wp_send_json_error( array( 'message' => $result->get_error_message() ) ); + } + + wp_send_json_success( $result ); + } + + /** + * AJAX: Delete WordPress info files (readme.html, license.txt). + */ + public function ajax_delete_wp_file() { + check_ajax_referer( 'wpsp_admin' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_send_json_error( array( 'message' => __( 'Permission denied.', 'wp-security-pack' ) ) ); + } + + $file = isset( $_POST['file'] ) ? sanitize_file_name( wp_unslash( $_POST['file'] ) ) : ''; + + // Only allow specific safe files to be deleted. + $allowed_files = array( 'readme.html', 'license.txt' ); + + if ( ! in_array( $file, $allowed_files, true ) ) { + wp_send_json_error( array( 'message' => __( 'Invalid file.', 'wp-security-pack' ) ) ); + } + + $file_path = ABSPATH . $file; + + if ( ! file_exists( $file_path ) ) { + wp_send_json_error( array( 'message' => __( 'File not found.', 'wp-security-pack' ) ) ); + } + + // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink + if ( ! unlink( $file_path ) ) { + wp_send_json_error( array( 'message' => __( 'Failed to delete file. Check file permissions.', 'wp-security-pack' ) ) ); + } + + wp_send_json_success(); + } } diff --git a/wp-security-pack/includes/class-wpsp-db.php b/wp-security-pack/includes/class-wpsp-db.php index bbcbe49..80488f6 100644 --- a/wp-security-pack/includes/class-wpsp-db.php +++ b/wp-security-pack/includes/class-wpsp-db.php @@ -155,6 +155,15 @@ class WPSP_DB { // Delete options. delete_option( 'wpsp_settings' ); delete_option( 'wpsp_db_version' ); + delete_option( 'wpsp_malware_scan_results' ); + delete_option( 'wpsp_malware_last_scan' ); + delete_option( 'wpsp_malware_hashes' ); + delete_option( 'wpsp_malware_hashes_updated' ); + delete_option( 'wpsp_malware_scan_stats' ); + delete_option( 'wpsp_quarantined_files' ); + delete_option( 'wpsp_file_scan_results' ); + delete_option( 'wpsp_file_last_scan' ); + delete_option( 'wpsp_file_baseline' ); // Delete transients. delete_transient( 'wpsp_geo_cache' ); diff --git a/wp-security-pack/includes/class-wpsp-malware-scanner.php b/wp-security-pack/includes/class-wpsp-malware-scanner.php index 2a415b4..91889ed 100644 --- a/wp-security-pack/includes/class-wpsp-malware-scanner.php +++ b/wp-security-pack/includes/class-wpsp-malware-scanner.php @@ -108,27 +108,48 @@ class WPSP_Malware_Scanner { /** * Load known malware file hashes. * - * These are MD5 hashes of known malicious files. When a file matches, - * it's 100% confirmed malware - no false positives possible. + * Hashes can come from: + * 1. Local database (user-added) + * 2. Custom filter (wpsp_malware_hashes) */ private function load_malware_hashes() { - // Try to load from database (updated hashes). - $stored_hashes = get_option( self::HASH_DB_OPTION, array() ); - - if ( ! empty( $stored_hashes ) ) { - $this->malware_hashes = $stored_hashes; - return; - } - - // Hash database starts empty - relies on signature detection. - // Users can add hashes via the 'wpsp_malware_hashes' filter or - // by updating from a trusted source using update_hash_database(). - $this->malware_hashes = array(); + // Load from database (user-added or previously downloaded). + $this->malware_hashes = get_option( self::HASH_DB_OPTION, array() ); // Allow adding custom hashes via filter. $this->malware_hashes = apply_filters( 'wpsp_malware_hashes', $this->malware_hashes ); } + /** + * Add a hash to the local database. + * + * @param string $hash MD5 hash. + * @param string $name Malware name/description. + */ + public function add_hash_to_database( $hash, $name ) { + $hashes = get_option( self::HASH_DB_OPTION, array() ); + $hashes[ strtolower( $hash ) ] = $name; + update_option( self::HASH_DB_OPTION, $hashes ); + $this->malware_hashes[ strtolower( $hash ) ] = $name; + } + + /** + * Remove a hash from the local database. + * + * @param string $hash MD5 hash. + */ + public function remove_hash_from_database( $hash ) { + $hashes = get_option( self::HASH_DB_OPTION, array() ); + $hash = strtolower( $hash ); + if ( isset( $hashes[ $hash ] ) ) { + unset( $hashes[ $hash ] ); + update_option( self::HASH_DB_OPTION, $hashes ); + } + if ( isset( $this->malware_hashes[ $hash ] ) ) { + unset( $this->malware_hashes[ $hash ] ); + } + } + /** * Update malware hash database from remote source. * @@ -516,6 +537,8 @@ class WPSP_Malware_Scanner { * @return array */ public function scan_files( $paths = array() ) { + $start_time = microtime( true ); + if ( empty( $paths ) ) { // Only scan wp-content directories, NOT WordPress core. $paths = array( @@ -586,7 +609,18 @@ class WPSP_Malware_Scanner { } } + $duration = microtime( true ) - $start_time; + + // Save scan stats. update_option( self::LAST_SCAN_OPTION, time() ); + update_option( + 'wpsp_malware_scan_stats', + array( + 'files_scanned' => $file_count, + 'duration' => round( $duration, 2 ), + 'issues_found' => count( $results ), + ) + ); return $results; } @@ -763,4 +797,219 @@ class WPSP_Malware_Scanner { return isset( $colors[ $severity ] ) ? $colors[ $severity ] : '#6c757d'; } + + /** + * Get quarantine directory path. + * + * @return string + */ + public function get_quarantine_dir() { + $upload_dir = wp_upload_dir(); + $quarantine = $upload_dir['basedir'] . '/wpsp-quarantine'; + + if ( ! file_exists( $quarantine ) ) { + wp_mkdir_p( $quarantine ); + + // Protect quarantine directory. + $htaccess = $quarantine . '/.htaccess'; + if ( ! file_exists( $htaccess ) ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents + file_put_contents( $htaccess, "Deny from all\n" ); + } + + $index = $quarantine . '/index.php'; + if ( ! file_exists( $index ) ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents + file_put_contents( $index, "get_quarantine_dir(); + $file_hash = md5_file( $file_path ); + $timestamp = time(); + $original_name = basename( $file_path ); + $relative_path = str_replace( ABSPATH, '', $file_path ); + + // Create unique quarantine filename. + $quarantine_name = sprintf( + '%s_%s_%s.quarantined', + $timestamp, + $file_hash, + sanitize_file_name( $original_name ) + ); + $quarantine_path = $quarantine_dir . '/' . $quarantine_name; + + // Store metadata. + $metadata = array( + 'original_path' => $file_path, + 'relative_path' => $relative_path, + 'original_name' => $original_name, + 'file_hash' => $file_hash, + 'quarantined_at' => $timestamp, + 'quarantined_by' => get_current_user_id(), + 'file_size' => filesize( $file_path ), + ); + + // Move file to quarantine. + // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename + if ( ! rename( $file_path, $quarantine_path ) ) { + return new WP_Error( 'move_failed', __( 'Failed to move file to quarantine.', 'wp-security-pack' ) ); + } + + // Save metadata. + $meta_file = $quarantine_path . '.meta'; + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents + file_put_contents( $meta_file, wp_json_encode( $metadata, JSON_PRETTY_PRINT ) ); + + // Update quarantine list in options. + $quarantined = get_option( 'wpsp_quarantined_files', array() ); + $quarantined[ $quarantine_name ] = $metadata; + update_option( 'wpsp_quarantined_files', $quarantined ); + + // Remove from scan results if present. + $results = get_option( self::RESULTS_OPTION, array() ); + if ( isset( $results[ $file_path ] ) ) { + unset( $results[ $file_path ] ); + update_option( self::RESULTS_OPTION, $results ); + } + + return array( + 'success' => true, + 'quarantine_name' => $quarantine_name, + 'metadata' => $metadata, + ); + } + + /** + * Restore a file from quarantine. + * + * @param string $quarantine_name The quarantine filename. + * @return array|WP_Error Result or error. + */ + public function restore_file( $quarantine_name ) { + $quarantine_dir = $this->get_quarantine_dir(); + $quarantine_path = $quarantine_dir . '/' . $quarantine_name; + $meta_file = $quarantine_path . '.meta'; + + if ( ! file_exists( $quarantine_path ) ) { + return new WP_Error( 'file_not_found', __( 'Quarantined file not found.', 'wp-security-pack' ) ); + } + + // Get metadata. + $metadata = array(); + if ( file_exists( $meta_file ) ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + $metadata = json_decode( file_get_contents( $meta_file ), true ); + } + + if ( empty( $metadata['original_path'] ) ) { + return new WP_Error( 'no_metadata', __( 'Cannot restore: original path unknown.', 'wp-security-pack' ) ); + } + + $original_path = $metadata['original_path']; + + // Ensure parent directory exists. + $parent_dir = dirname( $original_path ); + if ( ! file_exists( $parent_dir ) ) { + wp_mkdir_p( $parent_dir ); + } + + // Restore file. + // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename + if ( ! rename( $quarantine_path, $original_path ) ) { + return new WP_Error( 'restore_failed', __( 'Failed to restore file.', 'wp-security-pack' ) ); + } + + // Clean up metadata file. + if ( file_exists( $meta_file ) ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink + unlink( $meta_file ); + } + + // Update quarantine list. + $quarantined = get_option( 'wpsp_quarantined_files', array() ); + if ( isset( $quarantined[ $quarantine_name ] ) ) { + unset( $quarantined[ $quarantine_name ] ); + update_option( 'wpsp_quarantined_files', $quarantined ); + } + + return array( + 'success' => true, + 'restored_path' => $original_path, + ); + } + + /** + * Delete a quarantined file permanently. + * + * @param string $quarantine_name The quarantine filename. + * @return array|WP_Error Result or error. + */ + public function delete_quarantined_file( $quarantine_name ) { + $quarantine_dir = $this->get_quarantine_dir(); + $quarantine_path = $quarantine_dir . '/' . $quarantine_name; + $meta_file = $quarantine_path . '.meta'; + + if ( ! file_exists( $quarantine_path ) ) { + return new WP_Error( 'file_not_found', __( 'Quarantined file not found.', 'wp-security-pack' ) ); + } + + // Delete file. + // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink + if ( ! unlink( $quarantine_path ) ) { + return new WP_Error( 'delete_failed', __( 'Failed to delete file.', 'wp-security-pack' ) ); + } + + // Clean up metadata file. + if ( file_exists( $meta_file ) ) { + // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink + unlink( $meta_file ); + } + + // Update quarantine list. + $quarantined = get_option( 'wpsp_quarantined_files', array() ); + if ( isset( $quarantined[ $quarantine_name ] ) ) { + unset( $quarantined[ $quarantine_name ] ); + update_option( 'wpsp_quarantined_files', $quarantined ); + } + + return array( 'success' => true ); + } + + /** + * Get list of quarantined files. + * + * @return array + */ + public function get_quarantined_files() { + return get_option( 'wpsp_quarantined_files', array() ); + } + + /** + * Get count of quarantined files. + * + * @return int + */ + public function get_quarantine_count() { + return count( $this->get_quarantined_files() ); + } } diff --git a/wp-security-pack/wp-security-pack.php b/wp-security-pack/wp-security-pack.php index 0d910eb..058e708 100644 --- a/wp-security-pack/wp-security-pack.php +++ b/wp-security-pack/wp-security-pack.php @@ -1,7 +1,6 @@