load_signatures(); $this->load_malware_hashes(); if ( ! WP_Security_Pack::get_setting( 'malware_scan_enabled', true ) ) { return; } // Schedule weekly scan. add_action( 'wpsp_weekly_malware_scan', array( $this, 'run_scheduled_scan' ) ); if ( ! wp_next_scheduled( 'wpsp_weekly_malware_scan' ) ) { wp_schedule_event( time(), 'weekly', 'wpsp_weekly_malware_scan' ); } } /** * Load known malware file hashes. * * Hashes can come from: * 1. Local database (user-added) * 2. Custom filter (wpsp_malware_hashes) */ private function load_malware_hashes() { // 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. * * @param string $source_url URL to fetch hashes from (JSON format). * @return bool|WP_Error */ public function update_hash_database( $source_url = '' ) { if ( empty( $source_url ) ) { // Default: Could be a GitHub raw URL or your own endpoint. // For now, just return - users can provide their own source. return new WP_Error( 'no_source', __( 'No hash database source URL provided.', 'wp-security-pack' ) ); } $response = wp_remote_get( $source_url, array( 'timeout' => 30 ) ); if ( is_wp_error( $response ) ) { return $response; } $body = wp_remote_retrieve_body( $response ); $data = json_decode( $body, true ); if ( ! is_array( $data ) ) { return new WP_Error( 'invalid_data', __( 'Invalid hash database format.', 'wp-security-pack' ) ); } // Merge with existing hashes. $current_hashes = $this->malware_hashes; $new_hashes = array_merge( $current_hashes, $data ); update_option( self::HASH_DB_OPTION, $new_hashes ); update_option( self::HASH_DB_UPDATED_OPTION, time() ); $this->malware_hashes = $new_hashes; return true; } /** * Check if a file matches a known malware hash. * * @param string $file_path Path to file. * @return array|false Malware info if matched, false otherwise. */ public function check_file_hash( $file_path ) { if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) { return false; } $md5_hash = md5_file( $file_path ); if ( isset( $this->malware_hashes[ $md5_hash ] ) ) { return array( 'hash' => $md5_hash, 'name' => $this->malware_hashes[ $md5_hash ], 'method' => 'hash', ); } return false; } /** * Get hash database info. * * @return array */ public function get_hash_database_info() { return array( 'count' => count( $this->malware_hashes ), 'last_updated' => get_option( self::HASH_DB_UPDATED_OPTION, 0 ), 'source' => empty( get_option( self::HASH_DB_OPTION ) ) ? 'built-in' : 'updated', ); } /** * Load malware signatures. * * Patterns are organized by category and designed to minimize false positives * while catching real threats. We do NOT scan WordPress core files. */ private function load_signatures() { $this->signatures = array( // ===================================================================== // CRITICAL: Code Execution with Obfuscation // These patterns are almost always malicious. // ===================================================================== array( 'name' => 'Base64 Decode Execution', 'pattern' => '/\beval\s*\(\s*base64_decode\s*\(/i', 'severity' => 'critical', 'description' => 'Executing base64-encoded PHP code', ), array( 'name' => 'Gzinflate Execution', 'pattern' => '/\beval\s*\(\s*gzinflate\s*\(/i', 'severity' => 'critical', 'description' => 'Executing gzip-compressed PHP code', ), array( 'name' => 'Gzuncompress Execution', 'pattern' => '/\beval\s*\(\s*gzuncompress\s*\(/i', 'severity' => 'critical', 'description' => 'Executing compressed PHP code', ), array( 'name' => 'Str_rot13 Execution', 'pattern' => '/\beval\s*\(\s*str_rot13\s*\(/i', 'severity' => 'critical', 'description' => 'Executing ROT13-obfuscated PHP code', ), array( 'name' => 'Multiple Decode Layers', 'pattern' => '/base64_decode\s*\([^)]*base64_decode/is', 'severity' => 'critical', 'description' => 'Multiple layers of encoding (heavy obfuscation)', ), array( 'name' => 'Preg_replace /e Modifier', 'pattern' => '/preg_replace\s*\(\s*["\'][^"\']*\/[a-z]*e[a-z]*["\']/', 'severity' => 'critical', 'description' => 'Code execution via deprecated preg_replace /e modifier', ), // ===================================================================== // CRITICAL: User Input to Code Execution // Direct path from user input to code execution. // ===================================================================== array( 'name' => 'Eval with User Input', 'pattern' => '/\beval\s*\(\s*[\$\.\s]*\$_(POST|GET|REQUEST|COOKIE|SERVER|FILES)/i', 'severity' => 'critical', 'description' => 'Direct code execution from user input', ), array( 'name' => 'Assert with User Input', 'pattern' => '/\bassert\s*\(\s*[\$\.\s]*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'critical', 'description' => 'Code execution via assert() from user input', ), array( 'name' => 'Create_function with User Input', 'pattern' => '/\bcreate_function\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'critical', 'description' => 'Dynamic function creation with user input', ), array( 'name' => 'Call_user_func with User Input', 'pattern' => '/\bcall_user_func(_array)?\s*\(\s*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'critical', 'description' => 'Calling arbitrary function from user input', ), array( 'name' => 'Variable Function with User Input', 'pattern' => '/\$_(POST|GET|REQUEST|COOKIE)\s*\[[^\]]+\]\s*\(/i', 'severity' => 'critical', 'description' => 'Calling function name from user input', ), // ===================================================================== // CRITICAL: Shell Command Execution with User Input // ===================================================================== array( 'name' => 'Shell_exec with User Input', 'pattern' => '/\bshell_exec\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'critical', 'description' => 'Shell command execution with user input', ), array( 'name' => 'System with User Input', 'pattern' => '/\bsystem\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'critical', 'description' => 'System command execution with user input', ), array( 'name' => 'Passthru with User Input', 'pattern' => '/\bpassthru\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'critical', 'description' => 'Passthru command execution with user input', ), array( 'name' => 'Exec with User Input', 'pattern' => '/\bexec\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'critical', 'description' => 'Exec command execution with user input', ), array( 'name' => 'Popen with User Input', 'pattern' => '/\bpopen\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'critical', 'description' => 'Process opened with user-controlled command', ), array( 'name' => 'Proc_open with User Input', 'pattern' => '/\bproc_open\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'critical', 'description' => 'Process opened with user-controlled command', ), // ===================================================================== // CRITICAL: File Inclusion Vulnerabilities // ===================================================================== array( 'name' => 'Include with User Input', 'pattern' => '/\b(include|require|include_once|require_once)\s*\(?\s*[\$\.\s]*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'critical', 'description' => 'Local/Remote File Inclusion vulnerability', ), // ===================================================================== // CRITICAL: File Write Vulnerabilities // ===================================================================== array( 'name' => 'File_put_contents with User Input', 'pattern' => '/\bfile_put_contents\s*\([^,]*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'critical', 'description' => 'Writing to user-controlled file path', ), array( 'name' => 'Fwrite with User Content', 'pattern' => '/\bfwrite\s*\([^,]+,\s*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'high', 'description' => 'Writing user content to file', ), array( 'name' => 'Fopen with User Path', 'pattern' => '/\bfopen\s*\(\s*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'critical', 'description' => 'Opening user-controlled file path', ), array( 'name' => 'Unrestricted File Upload Path', 'pattern' => '/move_uploaded_file\s*\([^,]+,\s*[^)]*\$_(POST|GET|REQUEST)/i', 'severity' => 'critical', 'description' => 'Uploading file to user-controlled path', ), // ===================================================================== // HIGH: Known Backdoor Patterns // Specific patterns that indicate known malware structures. // ===================================================================== array( 'name' => 'Web Shell Upload Form', 'pattern' => '/]*enctype=["\']multipart\/form-data["\'][^>]*>.*]*type=["\']file["\'].*\$_FILES/is', 'severity' => 'high', 'description' => 'File upload form with immediate processing (potential backdoor)', ), array( 'name' => 'Eval POST/GET Pattern', 'pattern' => '/\beval\s*\(\s*\$_(POST|GET)\s*\[\s*["\'][a-z0-9_]+["\']\s*\]\s*\)/i', 'severity' => 'critical', 'description' => 'Classic backdoor pattern: eval($_POST[key])', ), array( 'name' => 'Base64 POST Execution', 'pattern' => '/\beval\s*\(\s*base64_decode\s*\(\s*\$_(POST|GET|REQUEST)/i', 'severity' => 'critical', 'description' => 'Executing base64-encoded user input', ), array( 'name' => 'Gunzip Eval Chain', 'pattern' => '/\beval\s*\(\s*gzuncompress\s*\(\s*base64_decode/i', 'severity' => 'critical', 'description' => 'Multi-layer deobfuscation chain', ), // ===================================================================== // HIGH: Obfuscation Indicators // ===================================================================== array( 'name' => 'Very Long Encoded String', 'pattern' => '/["\'][A-Za-z0-9+\/=]{1500,}["\']/s', 'severity' => 'high', 'description' => 'Extremely long encoded string (likely obfuscated malware)', ), array( 'name' => 'Hex Escape Sequence', 'pattern' => '/(\\\\x[0-9a-fA-F]{2}){20,}/i', 'severity' => 'high', 'description' => 'Long hex-encoded string (obfuscation technique)', ), array( 'name' => 'Chr() Obfuscation', 'pattern' => '/(\bchr\s*\(\s*\d+\s*\)\s*\.?\s*){10,}/i', 'severity' => 'high', 'description' => 'Building string from chr() calls (obfuscation)', ), array( 'name' => 'Array Character Building', 'pattern' => '/\$\w+\s*=\s*["\'][A-Za-z]+["\'];\s*\$\w+\s*=\s*\$\w+\[\d+\]\s*\.\s*\$\w+\[\d+\]/i', 'severity' => 'medium', 'description' => 'Building function names from array indices (obfuscation)', ), // ===================================================================== // HIGH: WordPress-Specific Attacks // ===================================================================== array( 'name' => 'WP User Creation Backdoor', 'pattern' => '/wp_create_user\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'critical', 'description' => 'Creating WordPress user from user input', ), array( 'name' => 'WP Insert User Backdoor', 'pattern' => '/wp_insert_user\s*\([^)]*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'critical', 'description' => 'Inserting WordPress user from user input', ), array( 'name' => 'WP Option Injection', 'pattern' => '/update_option\s*\(\s*\$_(POST|GET|REQUEST)/i', 'severity' => 'critical', 'description' => 'Updating arbitrary WordPress option from user input', ), array( 'name' => 'WP Auth Cookie Manipulation', 'pattern' => '/wp_set_auth_cookie\s*\([^)]*\$_(POST|GET|REQUEST)/i', 'severity' => 'critical', 'description' => 'Setting auth cookie from user input (authentication bypass)', ), array( 'name' => 'WP Role Escalation', 'pattern' => '/->set_role\s*\(\s*["\']administrator["\']\s*\)|->add_cap\s*\([^)]*\$_(POST|GET|REQUEST)/i', 'severity' => 'critical', 'description' => 'Privilege escalation attempt', ), // ===================================================================== // HIGH: Suspicious Patterns with Context // These require specific dangerous context to trigger. // ===================================================================== array( 'name' => 'Error Suppression with Eval', 'pattern' => '/@\s*eval\s*\(/i', 'severity' => 'high', 'description' => 'Eval with error suppression (hiding malicious activity)', ), array( 'name' => 'Remote Code Fetch and Execute', 'pattern' => '/eval\s*\(\s*file_get_contents\s*\(\s*["\']https?:\/\//i', 'severity' => 'critical', 'description' => 'Fetching and executing remote code', ), array( 'name' => 'CURL Fetch and Execute', 'pattern' => '/eval\s*\([^)]*curl_exec/is', 'severity' => 'critical', 'description' => 'Executing code fetched via CURL', ), array( 'name' => 'Dynamic URL Fetch with User Input', 'pattern' => '/file_get_contents\s*\(\s*\$_(POST|GET|REQUEST|COOKIE)/i', 'severity' => 'high', 'description' => 'Fetching content from user-controlled URL', ), ); // Allow adding custom signatures via filter. $this->signatures = apply_filters( 'wpsp_malware_signatures', $this->signatures ); } /** * Run scheduled scan. */ public function run_scheduled_scan() { $results = $this->scan_files(); if ( ! empty( $results ) ) { update_option( self::RESULTS_OPTION, $results ); $this->send_alert( $results ); } update_option( self::LAST_SCAN_OPTION, time() ); } /** * Scan files for malware signatures. * * Only scans plugins, themes, and uploads directories. * Does NOT scan WordPress core to avoid false positives. * * @param array $paths Paths to scan (default: wp-content directories only). * @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( WP_CONTENT_DIR . '/plugins/', WP_CONTENT_DIR . '/themes/', WP_CONTENT_DIR . '/uploads/', WP_CONTENT_DIR . '/mu-plugins/', ); } $results = array(); $file_count = 0; $max_files = 10000; // Limit to prevent timeout. foreach ( $paths as $path ) { if ( ! file_exists( $path ) ) { continue; } if ( is_file( $path ) ) { if ( ! $this->should_skip_file( $path ) ) { $file_results = $this->scan_file( $path ); if ( ! empty( $file_results ) ) { $results[ $path ] = $file_results; } } $file_count++; } else { $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $path, RecursiveDirectoryIterator::SKIP_DOTS ), RecursiveIteratorIterator::SELF_FIRST ); foreach ( $iterator as $file ) { if ( $file_count >= $max_files ) { break 2; } if ( ! $file->isFile() ) { continue; } $file_path = $file->getPathname(); $ext = strtolower( $file->getExtension() ); // Only scan PHP files. if ( 'php' !== $ext ) { continue; } // Skip large files (> 2MB). if ( $file->getSize() > 2 * 1024 * 1024 ) { continue; } // Skip known safe paths. if ( $this->should_skip_file( $file_path ) ) { continue; } $file_results = $this->scan_file( $file_path ); if ( ! empty( $file_results ) ) { $results[ $file_path ] = $file_results; } $file_count++; } } } $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; } /** * Check if a file should be skipped (known legitimate libraries). * * @param string $file_path File path to check. * @return bool True if file should be skipped. */ private function should_skip_file( $file_path ) { foreach ( $this->skip_paths as $skip ) { if ( stripos( $file_path, $skip ) !== false ) { return true; } } return false; } /** * Scan a single file. * * @param string $file_path File path. * @return array */ public function scan_file( $file_path ) { if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) { return array(); } $findings = array(); // First: Check against known malware hashes (100% accurate). $hash_match = $this->check_file_hash( $file_path ); if ( $hash_match ) { $findings[] = array( 'name' => 'Known Malware: ' . $hash_match['name'], 'severity' => 'critical', 'description' => 'File matches known malware hash (MD5: ' . $hash_match['hash'] . ')', 'match' => 'Hash match - confirmed malware', 'confirmed' => true, ); // Hash match is definitive - still scan for patterns but mark as confirmed. } // Second: Pattern-based detection. // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $content = file_get_contents( $file_path ); if ( ! empty( $content ) ) { foreach ( $this->signatures as $signature ) { if ( preg_match( $signature['pattern'], $content, $matches ) ) { $findings[] = array( 'name' => $signature['name'], 'severity' => $signature['severity'], 'description' => $signature['description'], 'match' => substr( $matches[0], 0, 100 ), 'confirmed' => false, ); } } } return $findings; } /** * Send email alert about scan results. * * @param array $results Scan results. */ private function send_alert( $results ) { if ( ! WP_Security_Pack::get_setting( 'email_alerts_enabled', false ) ) { return; } $email = WP_Security_Pack::get_setting( 'email_alerts_address', get_option( 'admin_email' ) ); $site_name = get_bloginfo( 'name' ); $site_url = home_url(); // Count by severity. $counts = array( 'critical' => 0, 'high' => 0, 'medium' => 0, 'low' => 0, ); foreach ( $results as $file => $findings ) { foreach ( $findings as $finding ) { $counts[ $finding['severity'] ]++; } } $subject = sprintf( /* translators: %s: Site name */ __( '[%s] Malware Scan Alert - Suspicious Files Found', 'wp-security-pack' ), $site_name ); $message = sprintf( /* translators: %s: Site URL */ __( "WP Security Pack malware scan has detected suspicious files on %s.\n\n", 'wp-security-pack' ), $site_url ); $message .= __( "Summary:\n", 'wp-security-pack' ); $message .= sprintf( __( "- Critical: %d\n", 'wp-security-pack' ), $counts['critical'] ); $message .= sprintf( __( "- High: %d\n", 'wp-security-pack' ), $counts['high'] ); $message .= sprintf( __( "- Medium: %d\n", 'wp-security-pack' ), $counts['medium'] ); $message .= sprintf( __( "- Low: %d\n\n", 'wp-security-pack' ), $counts['low'] ); $message .= __( "Files with issues:\n", 'wp-security-pack' ); $count = 0; foreach ( $results as $file => $findings ) { if ( $count >= 20 ) { $message .= sprintf( /* translators: %d: Number of additional files */ __( "... and %d more files\n", 'wp-security-pack' ), count( $results ) - 20 ); break; } $relative_path = str_replace( ABSPATH, '', $file ); $message .= "\n" . $relative_path . ":\n"; foreach ( $findings as $finding ) { $message .= sprintf( " - [%s] %s\n", strtoupper( $finding['severity'] ), $finding['name'] ); } $count++; } $message .= "\n" . __( "Please review these files in your WordPress admin panel under Settings > WP Security Pack.", 'wp-security-pack' ); $message .= "\n\n" . __( "Note: Some detections may be false positives. Review each file carefully before taking action.", 'wp-security-pack' ); wp_mail( $email, $subject, $message ); } /** * Get last scan results. * * @return array */ public function get_last_scan_results() { return array( 'time' => get_option( self::LAST_SCAN_OPTION, 0 ), 'results' => get_option( self::RESULTS_OPTION, array() ), ); } /** * Clear scan results. */ public function clear_results() { delete_option( self::RESULTS_OPTION ); } /** * Get severity color. * * @param string $severity Severity level. * @return string */ public static function get_severity_color( $severity ) { $colors = array( 'critical' => '#dc3545', 'high' => '#fd7e14', 'medium' => '#ffc107', 'low' => '#17a2b8', ); 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() ); } }