owner->raw_data['file']; list( $file_size, $file_created_at, $delete ) = $this->get_file_meta( $file ); $is_quarantinable = $delete ? false : $this->is_quarantinable( $file ); $quarantine_data = class_exists( 'WP_Defender\Component\Quarantine' ) ? wd_di()->get( Quarantine_Component::class )->scan_item_data( $this->owner ) : array( 'is_quarantinable' => $is_quarantinable ); return array_merge( array( 'id' => $this->owner->id, 'type' => Scan_Item::TYPE_SUSPICIOUS, 'file_name' => pathinfo( $file, PATHINFO_BASENAME ), 'full_path' => $file, 'date_added' => $file_created_at, 'size' => $file_size, 'deleted' => $delete, 'short_desc' => esc_html__( 'Suspicious function found', 'defender-security' ), ), $quarantine_data ); } /** * Ignores the scan item and marks it as successfully ignored. * * @return array An array with a message indicating the successful ignore action. */ public function ignore(): array { $scan = Scan::get_last(); $data = $this->owner->raw_data; $issue_name = '' . pathinfo( $data['file'], PATHINFO_BASENAME ) . ''; $res = $scan->ignore_issue( $this->owner->id ); if ( ! $res ) { return array( 'type_notice' => 'error', 'message' => $this->get_failed_ignore_result( $issue_name ), ); } return array( 'message' => sprintf( /* translators: %s: Scan issue name. */ esc_html__( 'You’ve successfully ignored the security issue related to %s.', 'defender-security' ), $issue_name ), ); } /** * Restores the ignored suspicious file. * * @return array An array with a message indicating the successful restoration action. */ public function unignore(): array { $scan = Scan::get_last(); $scan->unignore_issue( $this->owner->id ); return array( 'message' => esc_html__( 'The suspicious file has been successfully restored.', 'defender-security' ) ); } /** * Delete the file, or whole folder. * * @return array|WP_Error */ public function delete() { $data = $this->owner->raw_data; $scan = Scan::get_last(); $path = $data['file']; if ( ! file_exists( $path ) || ! is_readable( $path ) ) { $scan->remove_issue( $this->owner->id ); $this->log( sprintf( '%s is not readable and will be removed from the scan result', $path ), \WP_Defender\Controller\Scan::SCAN_LOG ); return array( 'message' => esc_html__( 'This item has been deleted.', 'defender-security' ) ); } /** * If the path inside theme or plugin folder, then we remove the whole plugin/theme for prevent fatal error. * If the path is to wp-config, we can't delete it. */ if ( defender_wp_config_path() === $path ) { return new WP_Error( Error_Code::INVALID, esc_html__( 'wp-config.php can\'t be removed. Please remove the suspicious code manually.', 'defender-security' ) ); } // If it's not relative path, which mean it is something lay on wp-content folder, we can delete it. if ( ! $this->delete_infected_file( $path ) ) { return $this->get_permission_error( $path ); } return $this->after_delete( $path, $scan, 'malware' ); } /** * Return the source code depending on the type of the issue. * * @return void|array */ public function pull_src() { global $wp_filesystem; // Initialize the WP filesystem, no more using 'file-put-contents' function. if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) { require_once ABSPATH . '/wp-admin/includes/file.php'; WP_Filesystem(); } $data = $this->owner->raw_data; $file_path = wp_normalize_path( $data['file'] ); $active = false; $type_src = ''; if ( ! file_exists( $file_path ) ) { return array( 'origin' => '', 'code' => '', ); } elseif ( false !== stripos( $file_path, wp_normalize_path( WP_PLUGIN_DIR ) ) ) { $active = $this->is_active_plugin( $file_path ); $type_src = 'plugin'; } elseif ( false !== stripos( $file_path, wp_normalize_path( $this->get_path_of_themes_dir() ) ) ) { $active = $this->is_active_theme( $file_path ); $type_src = 'theme'; } $markers = array(); foreach ( $data as $item ) { if ( isset( $item['catches'] ) ) { foreach ( $item['catches'] as $catch ) { $markers = array_merge( $markers, $catch['mapper'] ); } } } $success_data = array( 'origin' => $wp_filesystem->get_contents( $file_path ), 'mapper' => $markers, 'active' => $active, 'type_src' => $type_src, ); if ( 'plugin' === $type_src ) { $directory_name = $this->get_plugin_directory_name( $file_path ); $plugin_headers = $this->get_plugin_headers( $file_path ); if ( ! is_array( $plugin_headers ) || array() === $plugin_headers ) { return $success_data; } $plugin_headers = reset( $plugin_headers ); // Get the project url. if ( isset( $plugin_headers['PluginURI'] ) && '' !== $plugin_headers['PluginURI'] ) { $success_data['project_url'] = $plugin_headers['PluginURI']; } elseif ( isset( $directory_name ) && '' !== trim( $directory_name ) ) { $success_data['project_url'] = 'https://wp.org/plugins/' . $directory_name; } $success_data['local_file_state'] = $this->get_local_file_state( $file_path, $directory_name, $plugin_headers ); $success_data['author'] = $plugin_headers['Author'] ?? ''; $success_data['author_uri'] = $plugin_headers['AuthorURI'] ?? ''; } wp_send_json_success( $success_data ); } /** * Get local file state which used to handle repair or delete in front end. * * @param string $file_path File full path. * @param string $directory_name Plugin directory name. * @param array $plugin_headers Plugin headers. * * @return string Get local file state. */ private function get_local_file_state( $file_path, $directory_name, $plugin_headers ): string { $file_relative_path = $this->get_plugin_relative_path( $file_path ); $source_file_url = $this->get_file_url( $directory_name, $plugin_headers['Version'], $file_relative_path ); /** * Retrieves an instance of the File_Helper class. * * @var File_Helper $file_helper */ $file_helper = wd_di()->get( File_Helper::class ); $is_identical = $file_helper->is_identical_content( $file_path, $source_file_url ); if ( is_wp_error( $is_identical ) && 'http_404' === $is_identical->get_error_code() ) { // If file is not exists in remote then it may not be repaired. $local_file_state = self::REMOTE_FILE_NOT_EXISTS; } elseif ( false === $is_identical ) { $local_file_state = self::REMOTE_FILE_DIFFER; } else { $local_file_state = self::REMOTE_FILE_IDENTICAL; } return $local_file_state; } /** * A wrapper method for WP_Defender\Behavior\Scan_Item\Plugin_Integrity::resolve(). */ public function resolve() { $file_path = $this->owner->raw_data['file']; $directory_name = $this->get_plugin_directory_name( $file_path ); $plugin_headers = $this->get_plugin_headers( $file_path ); $plugin_headers = reset( $plugin_headers ); $local_file_state = $this->get_local_file_state( $file_path, $directory_name, $plugin_headers ); if ( 'REMOTE_FILE_DIFFER' !== $local_file_state ) { return; } /** * Retrieves an instance of the Plugin_Integrity class. * * @var Plugin_Integrity $plugin_integrity */ $plugin_integrity = wd_di()->get( Plugin_Integrity::class ); $this->owner->raw_data['type'] = 'modified'; $plugin_integrity->owner = $this->owner; return $plugin_integrity->resolve(); } }