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 @@