register_page( $this->get_title(), $this->slug, array( $this, 'main_view' ), $this->parent_slug ); $this->model = wd_di()->get( Scan_Settings::class ); $this->service = wd_di()->get( Scan_Component::class ); $this->quarantine_controller = wd_di()->get( Quarantine::class ); $this->is_unlocked = wd_di()->get( WPMUDEV::class )->is_pro(); $this->register_routes(); add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) ); add_action( 'wp_ajax_defender_process_scan', array( $this, 'process' ) ); add_action( 'wp_ajax_nopriv_defender_process_scan', array( $this, 'process' ) ); add_action( 'defender/async_scan', array( $this, 'process' ) ); // Clean up data after successful core update. add_action( '_core_updated_successfully', array( $this, 'clean_up_data' ) ); global $pagenow; // since 2.6.2. if ( is_admin() && 'plugins.php' === $pagenow && apply_filters( 'wd_display_vulnerability_warnings', true ) && wd_di()->get( WPMUDEV::class )->is_apikey_available() ) { $this->service->display_vulnerability_warnings(); } /** * Schedule a time to clear completed action scheduler logs. * * @var Network_Cron_Manager $network_cron_manager */ $network_cron_manager = wd_di()->get( Network_Cron_Manager::class ); $network_cron_manager->register_callback( 'wpdef_clear_scan_logs', array( $this, 'clear_scan_logs' ), WEEK_IN_SECONDS ); add_filter( 'heartbeat_nopriv_send', array( $this, 'nopriv_heartbeat' ), 10, 2 ); add_action( 'action_scheduler_completed_action', array( $this, 'scan_completed_analytics' ) ); } /** * Return the title of the page. * * @return string The title of the page. */ public function get_title(): string { return esc_html__( 'Issues', 'defender-security' ); } /** * Clean up data after core updating. * * @return void */ public function clean_up_data(): void { $this->service->clean_up(); } /** * Start a scan. * * @param Request $request Request object. * * @return Response * @defender_route * @defender_redirect */ public function start( Request $request ): Response { $data = $request->get_data( array( 'scan_type' => array( 'type' => 'string', 'sanitize' => 'sanitize_key', ), ) ); $scan_type = in_array( $data['scan_type'] ?? '', array( 'deep', 'malware' ), true ) ? $data['scan_type'] : 'malware'; $model = Model_Scan::create(); if ( is_object( $model ) && ! is_wp_error( $model ) ) { update_site_option( self::FIRST_SCAN_STARTED, '1' ); set_transient( 'defender_scan_triggered_by_' . $model->id, get_current_user_id(), HOUR_IN_SECONDS ); Model_Scan::set_scan_type( $scan_type ); $this->log( 'Initial ping self', self::SCAN_LOG ); $this->run_scan_mechanisms_from( 'scan' ); return new Response( true, array( 'status' => $model->status, 'status_text' => $model->get_status_text(), 'percent' => 0, 'scan_type' => $scan_type, ) ); } return new Response( false, array( 'message' => esc_html__( 'A scan is already in progress', 'defender-security' ), ) ); } /** * Use this for self ping, so it can both run in background and active mode with good performance. * * @return void * @defender_route * @is_public */ public function process() { $lock_filename = $this->service->get_lock_filename(); if ( ! $this->service->try_create_lock( $lock_filename ) ) { $this->log( 'Fallback as already a process is running', self::SCAN_LOG ); return; } // Check if the ping is from self or not. $ret = $this->service->process(); $this->log( 'process done, queue for next', self::SCAN_LOG ); if ( false === $ret ) { // Ping self. $this->log( 'Scan not done, pinging', self::SCAN_LOG ); $this->service->remove_lock( $lock_filename ); $this->process(); } else { $this->queue_to_sync_with_hub(); $this->service->remove_lock( $lock_filename ); } } /** * Query status. * * @return Response * @defender_route * @defender_redirect */ public function status(): Response { $scan_type = Model_Scan::get_scan_type(); $idle_scan = wd_di()->get( Model_Scan::class )->get_idle(); if ( is_object( $idle_scan ) ) { $this->service->update_idle_scan_status(); $response = $this->get_status_response_data( $idle_scan, $scan_type ); return new Response( true, $response ); } $checksum_issue = get_site_option( Core_Integrity::ISSUE_CHECKSUMS, 'false' ); $checksum_scan = Model_Scan::get_core_check(); if ( 'false' !== $checksum_issue && is_object( $checksum_scan ) ) { $this->service->update_idle_scan_status_by_checksum_issue( $checksum_scan ); $response = $this->get_status_response_data( $checksum_scan, $scan_type ); return new Response( true, $response ); } $scan = Model_Scan::get_active(); if ( is_object( $scan ) ) { $response = $this->get_status_response_data( $scan, $scan_type ); return new Response( true, $response ); } $scan = Model_Scan::get_last(); if ( is_object( $scan ) && ! is_wp_error( $scan ) ) { $response = array_merge( $this->get_status_response_data( $scan, $scan_type ), $this->get_last_scan_time_data( $scan ) ); $response['message'] = __( 'Malware scan completed successfully!', 'defender-security' ); $response['scan_type'] = $scan_type; if ( 'deep' === $scan_type ) { $security_tweaks = wd_di()->get( \WP_Defender\Controller\Security_Tweaks::class )->dashboard_widget(); $response['hardening_count'] = (int) ( $security_tweaks['summary']['issues_count'] ?? 0 ); } if ( isset( $this->quarantine_controller ) ) { $response['quarantine'] = $this->quarantine_controller->data_frontend()['list'] ?? array(); } return new Response( true, $response ); } return new Response( false, array( 'message' => esc_html__( 'Error during scanning', 'defender-security' ), ) ); } /** * Build the compatible status payload and consume its queued messages. * * @param Model_Scan $scan Scan model used to build the status payload. * @param string $scan_type Current scan type. */ private function get_status_response_data( Model_Scan $scan, string $scan_type ): array { $response = $scan->to_array(); $response['status_text'] = $response['status_text'] ?? $scan->get_status_text(); $response['percent'] = $response['percent'] ?? $scan->percent; $response['scan_type'] = $scan_type; $response['status_messages'] = $scan->drain_status_messages(); return $response; } /** * Cancel current scan. * * @return Response * @defender_route * @defender_redirect */ public function cancel(): Response { $component = wd_di()->get( Scan_Component::class ); $component->cancel_a_scan(); Model_Scan::clear_scan_type(); $last = Model_Scan::get_last(); if ( is_object( $last ) && ! is_wp_error( $last ) ) { $last = $last->to_array(); } return new Response( true, array( 'scan' => $last, ) ); } /** * Track scan item action analytics. * * @param Scan_Item $scan_item Individual item of scan issues list. * @param string $intention What action is going to be executed. */ private function item_action_analytics( Scan_Item $scan_item, string $intention ) { $allowed_intentions = Scan_Component::get_intentions(); $event_name = 'def_threat_resolved'; if ( in_array( $intention, $allowed_intentions, true ) ) { $intention_desc = array( 'resolve' => 'Safe Repair', 'ignore' => 'Ignore', 'delete' => 'Delete', 'unignore' => 'Unignore', 'quarantine' => 'Safe Repair & Quarantine', ); $resolution_method = $intention_desc[ $intention ]; $threat_type = ''; if ( Scan_Item::TYPE_INTEGRITY === $scan_item->type ) { // Track Repair-actions. if ( in_array( $intention, array( 'resolve', 'quarantine' ), true ) ) { $threat_type = 'core file modified'; } else { $threat_type = 'Unknown file in WordPress core'; } } elseif ( Scan_Item::TYPE_PLUGIN_CHECK === $scan_item->type ) { $raw_data = $scan_item->raw_data; if ( isset( $raw_data['type'] ) && 'modified' === $raw_data['type'] ) { $threat_type = 'plugin file modified'; } } elseif ( Scan_Item::TYPE_VULNERABILITY === $scan_item->type ) { $threat_type = 'Vulnerability'; if ( 'resolve' === $intention ) { $resolution_method = 'Update'; } } elseif ( Scan_Item::TYPE_SUSPICIOUS === $scan_item->type ) { $threat_type = 'Suspicious function'; } elseif ( in_array( $scan_item->type, Model_Scan::get_abandoned_types(), true ) ) { $threat_type = 'Outdated & removed plugins'; } $this->track_feature( $event_name, array( 'Resolution Method' => $resolution_method, 'Threat type' => $threat_type, ) ); } } /** * A central controller to pass any request from frontend to scan item. * * @param Request $request Request object. * * @return Response * @defender_route */ public function item_action( Request $request ): Response { $data = $request->get_data( array( 'id' => array( 'type' => 'int', 'sanitize' => 'sanitize_text_field', ), 'intention' => array( 'type' => 'string', 'sanitize' => 'sanitize_text_field', ), 'parent_action' => array( 'type' => 'string', 'sanitize' => 'sanitize_text_field', ), ) ); $id = $data['id'] ?? false; $intention = $data['intention'] ?? false; // Get allowed intentions. $allowed_intentions = Scan_Component::get_intentions(); $allowed_intentions[] = 'pull_src'; if ( false === $id || false === $intention || ! in_array( $intention, $allowed_intentions, true ) ) { return new Response( false, array() ); } $scan = Model_Scan::get_last(); if ( $scan instanceof Model_Scan ) { $item = $scan->get_issue( $id ); if ( is_object( $item ) && $item->has_method( $intention ) ) { if ( 'resolve' === $intention ) { $result = $item->resolve(); } elseif ( 'quarantine' === $intention ) { $result = $item->quarantine( $data['parent_action'], $item->owner ); } elseif ( 'delete' === $intention ) { $result = $item->delete(); } elseif ( 'ignore' === $intention ) { $result = $item->ignore(); } elseif ( 'unignore' === $intention ) { $result = $item->unignore(); } elseif ( 'pull_src' === $intention ) { $result = $item->pull_src(); } // Maybe track. if ( $this->is_tracking_active() ) { $this->item_action_analytics( $item, $intention ); } if ( is_wp_error( $result ) ) { return new Response( false, array( 'message' => $result->get_error_message(), ) ); } elseif ( isset( $result['type_notice'] ) ) { return new Response( true, $result ); } elseif ( isset( $result['url'] ) ) { // Without message and interval args. return new Response( true, array( 'redirect' => $result['url'] ) ); } $this->queue_to_sync_with_hub(); // Refresh scan instance. $scan = Model_Scan::get_last(); if ( $scan instanceof Model_Scan ) { $result['scan'] = $scan->to_array(); if ( 'quarantine' === $intention && isset( $this->quarantine_controller ) ) { $result['quarantine'] = $this->quarantine_controller->data_frontend()['list'] ?? array(); } $success = true; if ( isset( $result['success'] ) && false === $result['success'] ) { $success = false; } return new Response( $success, $result ); } } } return new Response( false, array() ); } /** * Process for bulk action. * There is no Update-intention because it is a lengthy process. There may not be enough execution time. * * @param Request $request Request object. * * @defender_route * @return Response */ public function bulk_action( Request $request ): Response { $data = $request->get_data( array( 'items' => array( 'type' => 'array', 'sanitize' => 'sanitize_text_field', ), 'bulk' => array( 'type' => 'string', 'sanitize' => 'sanitize_text_field', ), ) ); $items = $data['items'] ?? array(); $intention = $data['bulk'] ?? false; if ( ! is_array( $items ) || array() === $items || ! in_array( $intention, array( 'ignore', 'unignore', 'delete' ), true ) ) { return new Response( false, array() ); } // Try to get Scan. $scan = Model_Scan::get_last(); if ( ! is_object( $scan ) ) { return new Response( false, array() ); } $is_delete = false; $delete_items = array(); $none_delete_items = array(); $sync_hub = false; foreach ( $items as $id ) { if ( 'ignore' === $intention ) { $sync_hub = $scan->ignore_issue( (int) $id ); } elseif ( 'unignore' === $intention ) { $sync_hub = $scan->unignore_issue( (int) $id ); } elseif ( 'delete' === $intention ) { $item = $scan->get_issue( (int) $id ); // Work with every item. if ( is_object( $item ) && $item->has_method( $intention ) ) { $item_result = $item->delete(); if ( is_wp_error( $item_result ) ) { $none_delete_items[] = $item_result->get_error_message(); } elseif ( isset( $item_result['type_notice'] ) ) { return new Response( true, $item_result ); } elseif ( isset( $item_result['collect_type'] ) ) { $is_delete = true; $delete_items[] = $item_result['message']; } // If there is any error, no need to sync data. $sync_hub = true; } } } if ( $sync_hub ) { $this->queue_to_sync_with_hub(); } $result = array(); if ( array() !== $none_delete_items ) { $result['message'] = sprintf( /* translators: %s: Vulnerability item(es) */ _n( 'Defender doesn\'t have enough permission to remove this file: %s', 'Defender doesn\'t have enough permission to remove these files: %s', count( $none_delete_items ), 'defender-security' ), '
' . implode( PHP_EOL, $none_delete_items ) . '' ); } elseif ( $is_delete ) { $result['message'] = sprintf( /* translators: %s: Vulnerability item(es) */ esc_html__( '%s has (have) been deleted', 'defender-security' ), implode( ', ', $delete_items ) ); } // Refresh scan instance. $scan = Model_Scan::get_last(); $result['scan'] = $scan->to_array(); return new Response( array() === $none_delete_items, $result ); } /** * Save settings. * * @param Request $request The request object containing new settings data. * * @return Response * @since 2.7.0 Add Scheduled Scanning to Malware settings and hide it on Malware Scanning - Reporting. * Also, the backward compatibility of settings for Scan and Malware_Report models. * @defender_route */ public function save_settings( Request $request ): Response { $data = $request->get_data_by_model( $this->model ); // Prepare for the state's change. $old_integrity_check_state = $this->model->integrity_check; // Case#1: inherit the parent's state to nested options. if ( $old_integrity_check_state !== $data['integrity_check'] ) { $data['check_core'] = $data['integrity_check']; $data['check_plugins'] = $data['integrity_check']; } // Case#2: Suspicious code is activated BUT File change detection is deactivated then show the notice. if ( $data['scan_malware'] && ! $data['integrity_check'] ) { $response = array( 'type_notice' => 'info', 'message' => sprintf( /* translators: 1. Open tag. 2. Close tag. 3. Open tag. 4. Close tag. */ esc_html__( 'To reduce false-positive results, we recommend enabling %1$sFile change detection%2$s options for all scan types while the %3$sSuspicious code%4$s option is enabled.', 'defender-security' ), '', '', '', '' ), ); } else { $response = array( 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ), 'auto_close' => true, ); } $before_import_schedule = $this->model->quarantine_expire_schedule; $this->model->import( $data ); if ( $this->model->validate() ) { if ( class_exists( 'WP_Defender\Component\Quarantine' ) ) { $quarantine_component = wd_di()->get( Quarantine_Component::class ); $quarantine_component->reschedule_file_expiry_cron( $before_import_schedule, $data['quarantine_expire_schedule'] ); } $this->model->save(); Config_Hub_Helper::set_clear_active_flag(); return new Response( true, array_merge( $response, $this->data_frontend() ) ); } else { return new Response( false, array_merge( array( 'message' => $this->model->get_formatted_errors(), ), $this->data_frontend() ) ); } } /** * Get the issues mainly for pagination request. * * @param Request $request The request object. * * @return Response * @defender_route */ public function get_issues( Request $request ): Response { $data = $request->get_data( array( 'scenario' => array( 'type' => 'string', 'sanitize' => 'sanitize_text_field', ), 'type' => array( 'type' => 'array', 'sanitize' => 'sanitize_text_field', ), 'per_page' => array( 'type' => 'string', 'sanitize' => 'intval', ), 'paged' => array( 'type' => 'int', 'sanitize' => 'intval', ), ) ); // Validate the request. $v = new Validator( $data, array() ); $v->rule( 'required', array( 'scenario', 'type', 'per_page', 'paged' ) ); if ( ! $v->validate() ) { return new Response( false, array( 'message' => esc_html__( 'Wrong scan issue data.', 'defender-security' ), ) ); } $scan = Model_Scan::get_last(); $issues = $scan->to_array( $data['per_page'], $data['paged'], $this->normalize_issue_types( $data['type'] ), $data['scenario'] ); $response = array( 'issue' => $issues['issues_items'], 'ignored' => $issues['ignored_items'], 'paging' => $issues['paging'], 'count' => $issues['count'], ); if ( class_exists( 'WP_Defender\Controller\Quarantine' ) ) { $response['quarantine'] = $this->quarantine_controller->data_frontend()['list'] ?? array(); } return new Response( true, $response ); } /** * Normalize issue type filters from the legacy and redesign payloads. * * @param array|string $types Requested issue type(s). * * @return array|string */ private function normalize_issue_types( $types ) { $type_aliases = array( 'core' => Scan_Item::TYPE_INTEGRITY, 'plugin' => Scan_Item::TYPE_PLUGIN_CHECK, 'suspicious' => Scan_Item::TYPE_SUSPICIOUS, 'known_vulnerability' => Scan_Item::TYPE_VULNERABILITY, 'plugin_closed' => Scan_Item::TYPE_PLUGIN_CLOSED, 'plugin_outdated' => Scan_Item::TYPE_PLUGIN_OUTDATED, ); $types = is_array( $types ) ? $types : array( $types ); $types = array_filter( array_map( static function ( $type ) use ( $type_aliases ) { return $type_aliases[ $type ] ?? $type; }, $types ), 'boolval' ); $types = array_values( array_unique( $types ) ); if ( 1 === count( $types ) ) { return $types[0]; } return $types; } /** * Get relative and exact display times for the last completed scan. * * @param Model_Scan|null $last Last completed scan. * * @return array */ private function get_last_scan_time_data( $last ): array { if ( ! is_object( $last ) || ! isset( $last->date_start ) || '' === $last->date_start ) { return array( 'last_scan' => '', 'last_scan_time' => '', ); } $last_scan_timestamp = strtotime( $last->date_start . ' UTC' ); $time_difference = time() - $last_scan_timestamp; $data = array( 'last_scan' => sprintf( /* translators: %s: human-readable time difference, e.g. "5 minutes ago" */ __( 'Scanned %s ago', 'defender-security' ), human_time_diff( $last_scan_timestamp ) ), ); if ( $time_difference < DAY_IN_SECONDS ) { $data['last_scan_time'] = wp_date( 'g:i A', $last_scan_timestamp ); } elseif ( $time_difference < YEAR_IN_SECONDS ) { $data['last_scan_time'] = wp_date( 'D, j M g:i A', $last_scan_timestamp ); } else { $data['last_scan_time'] = wp_date( 'j M Y, g:i A', $last_scan_timestamp ); } return $data; } /** * Returns scan result data for the frontend on page load. * * @return array */ public function get_initial_scan_data(): array { $scan = Model_Scan::get_active(); $last = Model_Scan::get_last(); $per_page = self::DEFAULT_PER_PAGE; $paged = 1; if ( ! is_object( $scan ) && ! is_object( $last ) ) { $scan_data = null; } elseif ( is_object( $scan ) && is_object( $last ) ) { // If an active scan exists AND there's a previous completed scan, // merge the active scan's progress with the last scan's issue data. // This ensures that during a page refresh while scanning, users still // see the previous scan results while the new scan is in progress. $scan_data = $scan->to_array( $per_page, $paged ); $last_data = $last->to_array( $per_page, $paged ); // Preserve previous scan's issue data. $scan_data['issues_items'] = $last_data['issues_items'] ?? array(); $scan_data['ignored_items'] = $last_data['ignored_items'] ?? array(); $scan_data['count'] = $last_data['count'] ?? array(); $scan_data['paging'] = $last_data['paging'] ?? array(); } elseif ( is_object( $scan ) ) { $scan_data = $scan->to_array( $per_page, $paged ); } else { $scan_data = $last->to_array( $per_page, $paged ); } $first_scan_started = get_site_option( self::FIRST_SCAN_STARTED, null ); if ( null === $first_scan_started ) { // Existing installations backward compatibility. $first_scan_started = is_object( $scan ) || is_object( $last ); } else { $first_scan_started = '1' === (string) $first_scan_started; } $data = array( 'scan' => $scan_data, 'has_first_scan_started' => $first_scan_started, 'isEnabledScanType' => $this->service->is_any_scan_type_active(), ); // Always expose the last completed scan time at the outer level so the // dashboard can show it even while a new scan is actively running. $data = array_merge( $data, $this->get_last_scan_time_data( $last ) ); if ( isset( $this->quarantine_controller ) ) { $data['quarantine'] = array( 'list' => $this->quarantine_controller->data_frontend()['list'] ?? array(), ); } // Display Rate notice. Without the rating's type. $data['isRatingDisplayed'] = defender_is_wp_org_version() && is_array( $scan_data ) && isset( $data['scan']['issues_items'] ) && Rate_Component::is_displayed_in_redesigned_version( count( $data['scan']['issues_items'] ) ); return array( 'scan' => $data ); } /** * Render the main page. * * @return void */ public function main_view(): void { $this->render( 'main' ); } /** * Enqueues scripts and styles for this page. * Only enqueues assets if the page is active. */ public function enqueue_assets() { if ( ! $this->is_page_active() ) { return; } $handle = 'defender-ui-scan'; wp_enqueue_script( $handle, WP_DEFENDER_BASE_URL . 'assets/js/scan-ui.js', array( 'def-vue', 'def-manifest', 'def-core-ui', 'defender', 'wp-i18n' ), DEFENDER_VERSION, true ); wp_set_script_translations( $handle, 'wpdef' ); $scan_routes_data = $this->dump_routes_and_nonces(); $quarantine_routes_data = $this->quarantine_controller->dump_routes_and_nonces(); if ( defender_is_wp_org_version() ) { $rate_routes_nonces = wd_di()->get( \WP_Defender\Controller\Rate::class )->dump_routes_and_nonces(); $rate_routes = $rate_routes_nonces['routes']; $rate_nonces = $rate_routes_nonces['nonces']; } else { $rate_routes = array(); $rate_nonces = array(); } wp_localize_script( $handle, 'defenderUIData', array_merge( $this->get_shared_data(), array( 'routes' => array_merge( $scan_routes_data['routes'], $quarantine_routes_data['routes'], $rate_routes ), 'nonces' => array_merge( $scan_routes_data['nonces'], $quarantine_routes_data['nonces'], $rate_nonces ), ), $this->get_initial_scan_data() ) ); wp_enqueue_style( $handle, WP_DEFENDER_BASE_URL . 'assets/css/core-ui.css', array(), DEFENDER_VERSION ); $this->enqueue_main_assets(); } /** * Converts the current object state to an array. * * @return array The array representation of the object. */ public function to_array(): array { $scan = Model_Scan::get_active(); $last = Model_Scan::get_last(); if ( ! is_object( $scan ) && ! is_object( $last ) ) { $scan = null; } else { $scan = is_object( $scan ) ? $scan->to_array() : $last->to_array(); } return array_merge( array( 'scan' => $scan, 'report' => array( 'enabled' => true, 'frequency' => 'weekly', ), ), $this->dump_routes_and_nonces() ); } /** * Removes settings for all submodules. */ public function remove_settings(): void { ( new Scan_Settings() )->delete(); } /** * Delete all the data & the cache. */ public function remove_data(): void { delete_site_option( self::FIRST_SCAN_STARTED ); delete_site_option( Model_Scan::IGNORE_INDEXER ); delete_site_option( Model_Scan::OPTION_SCAN_TYPE ); Model_Scan::clear_all_status_messages(); delete_site_option( Core_Integrity::ISSUE_CHECKSUMS ); delete_site_transient( Plugin_Integrity::$org_slugs ); delete_site_transient( Plugin_Integrity::$org_responses ); } /** * Provides data for the frontend. * * @return array An array of data for the frontend. */ public function data_frontend(): array { $scan = Model_Scan::get_active(); $last = Model_Scan::get_last(); $per_page = 10; $paged = 1; if ( ! is_object( $scan ) && ! is_object( $last ) ) { $scan = null; } else { $scan = is_object( $scan ) ? $scan->to_array( $per_page, $paged ) : $last->to_array( $per_page, $paged ); } $settings = new Scan_Settings(); $report = wd_di()->get( Malware_Report::class ); $scan['isEnabledScanType'] = $this->service->is_any_scan_type_active(); $data = array( 'scan' => $scan, 'settings' => $settings->export(), 'notification' => $report->to_string(), 'misc' => array( 'labels' => $settings->labels(), ), ); $data['quarantine'] = $this->quarantine_controller->data_frontend(); return array_merge( $data, $this->dump_routes_and_nonces() ); } /** * Imports data into the model. * * @param array $data Data to be imported into the model. */ public function import_data( array $data ) { $model = $this->model; if ( array() === $data ) { $model->scheduled_scanning = false; $model->frequency = 'weekly'; $model->day_n = 1; $model->day = 'sunday'; $model->time = '4:00'; $model->save(); } else { $model->import( $data ); if ( $model->validate() ) { $model->save(); } } } /** * Exports strings. * * @return array An array of strings. */ public function export_strings(): array { $strings = array(); if ( $this->service->is_any_scan_type_active() ) { $strings[] = esc_html__( 'Active', 'defender-security' ); } else { $strings[] = esc_html__( 'Inactive', 'defender-security' ); } $scan_notification = new Malware_Notification(); if ( 'enabled' === $scan_notification->status ) { $strings[] = esc_html__( 'Email notifications active', 'defender-security' ); } $strings[] = sprintf( /* translators: %s: Html for Pro-tag. */ esc_html__( 'Scheduled scan inactive %s', 'defender-security' ), 'Pro' ); return $strings; } /** * Generates configuration strings based on the provided configuration. * * @param array $config Configuration data. * * @return array Returns an array of configuration strings. */ public function config_strings( array $config ): array { $strings = array(); $strings[] = $this->service->check_scan_active_by( $config ) ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ); if ( 'enabled' === $config['notification'] ) { $strings[] = esc_html__( 'Email notifications active', 'defender-security' ); } // No Pro membership and No hosted site. if ( ! $this->is_unlocked ) { $strings[] = sprintf( /* translators: %s: Html for Pro-tag. */ esc_html__( 'Scheduled scan inactive %s', 'defender-security' ), 'Pro' ); } return $strings; } /** * Run different scan actions based on the scan location. * * @param string $type Denotes type of the scan from the following 4 possible values: scan, install, hub or report. * * @return void */ public function run_scan_mechanisms_from( $type ) { $this->service->gather_actioned_plugin_details(); $this->do_async_scan( $type ); } /** * Triggers the asynchronous scan. * * @param string $type Denotes type of the scan from the following 4 possible values: scan, install, hub or report. * * @return void */ public function do_async_scan( string $type ): void { wd_di()->get( Model_Scan::class )->delete_idle(); // Delete the slug from the previous scan. delete_site_option( Core_Integrity::ISSUE_CHECKSUMS ); if ( function_exists( 'as_enqueue_async_action' ) ) { as_enqueue_async_action( 'defender/async_scan', array( 'type' => $type, ), 'defender' ); } } /** * Clear completed action scheduler logs. * * @return void * @since 2.6.5 */ public function clear_scan_logs(): void { $scan_component = wd_di()->get( Scan_Component::class ); $result = $scan_component::clear_logs(); if ( isset( $result['error'] ) ) { $this->log( 'WP CRON Error : ' . $result['error'], self::SCAN_LOG ); } } /** * When user session is expired and scan is running, then don't login via heartbeat modal. * * @param array $response The no-priv Heartbeat response. * @param string $screen_id The screen id. * * @return mixed * @since 3.11.0 */ public function nopriv_heartbeat( $response, $screen_id ) { if ( false !== strpos( $screen_id, $this->slug ) ) { $scan = Model_Scan::get_active(); if ( is_object( $scan ) ) { $response['wp-auth-check'] = true; } } return $response; } /** * Triggers and send analytics data on scan completed. * * @param int $action_id Action ID. * * @return void */ public function scan_completed_analytics( $action_id ) { if ( class_exists( ActionScheduler::class ) && method_exists( ActionScheduler::class, 'store' ) && 'defender' === ActionScheduler::store()->fetch_action( $action_id )->get_group() ) { $scan_analytics = wd_di()->get( Scan_Analytics::class ); $scan_model = wd_di()->get( Model_Scan::class ); $analytics_data = $scan_analytics->scan_completed( $scan_model ); if ( array() === $analytics_data ) { return; } $this->track_feature( $analytics_data['event'], $analytics_data['data'] ); } } }