(int) $option_timestamp; } /** * Checks if we should consider the stored cache or bypass it * * @return boolean */ public static function should_use_cache() { return ! ( ( defined( 'JETPACK_PROTECT_DEV__BYPASS_CACHE' ) && JETPACK_PROTECT_DEV__BYPASS_CACHE ) ); } /** * Gets the current cached history * * @return bool|array False if value is not found. Array with values if cache is found. */ public static function get_from_options() { return maybe_unserialize( get_option( static::OPTION_NAME ) ); } /** * Updated the cached history and its timestamp * * @param array $history The new history to be cached. * @return void */ public static function update_history_option( $history ) { // TODO: Sanitize $history. update_option( static::OPTION_NAME, maybe_serialize( $history ) ); update_option( static::OPTION_TIMESTAMP_NAME, time() + static::OPTION_EXPIRES_AFTER ); } /** * Delete the cached history and its timestamp * * @return bool Whether all related history options were successfully deleted. */ public static function delete_option() { $option_deleted = delete_option( static::OPTION_NAME ); $option_timestamp_deleted = delete_option( static::OPTION_TIMESTAMP_NAME ); return $option_deleted && $option_timestamp_deleted; } /** * Gets the current history of the Jetpack Protect checks * * @param bool $refresh_from_wpcom Refresh the local plan and history cache from wpcom. * @return History_Model|bool */ public static function get_scan_history( $refresh_from_wpcom = false ) { $has_required_plan = Plan::has_required_plan(); if ( ! $has_required_plan ) { return false; } if ( self::$history !== null ) { return self::$history; } if ( $refresh_from_wpcom || ! self::should_use_cache() || self::is_cache_expired() ) { $history = self::fetch_from_api(); } else { $history = self::get_from_options(); } if ( is_wp_error( $history ) ) { $history = new History_Model( array( 'error' => true, 'error_code' => $history->get_error_code(), 'error_message' => $history->get_error_message(), ) ); } else { $history = self::normalize_api_data( $history ); } self::$history = $history; return $history; } /** * Gets the Scan API endpoint * * @return WP_Error|string */ public static function get_api_url() { $blog_id = Jetpack_Options::get_option( 'id' ); $is_connected = ( new Connection_Manager() )->is_connected(); if ( ! $blog_id || ! $is_connected ) { return new WP_Error( 'site_not_connected' ); } $api_url = sprintf( self::SCAN_HISTORY_API_BASE, $blog_id ); return $api_url; } /** * Fetches the history data from the Scan API * * @return WP_Error|array */ public static function fetch_from_api() { $api_url = self::get_api_url(); if ( is_wp_error( $api_url ) ) { return $api_url; } $response = Client::wpcom_json_api_request_as_blog( $api_url, '2', array( 'method' => 'GET', 'timeout' => 30, ), null, 'wpcom' ); $response_code = wp_remote_retrieve_response_code( $response ); if ( is_wp_error( $response ) || 200 !== $response_code || empty( $response['body'] ) ) { return new WP_Error( 'failed_fetching_status', 'Failed to fetch Scan history from the server', array( 'status' => $response_code ) ); } $body = json_decode( wp_remote_retrieve_body( $response ) ); $body->last_checked = ( new \DateTime() )->format( 'Y-m-d H:i:s' ); self::update_history_option( $body ); return $body; } /** * Normalize API Data * Formats the payload from the Scan API into an instance of History_Model. * * @phan-suppress PhanDeprecatedProperty -- Maintaining backwards compatibility. * * @param object $scan_data The data returned by the scan API. * @return History_Model */ private static function normalize_api_data( $scan_data ) { $history = new History_Model(); $history->num_threats = 0; $history->num_core_threats = 0; $history->num_plugins_threats = 0; $history->num_themes_threats = 0; $history->last_checked = $scan_data->last_checked; if ( empty( $scan_data->threats ) || ! is_array( $scan_data->threats ) ) { return $history; } foreach ( $scan_data->threats as $threat ) { if ( isset( $threat->extension->type ) ) { if ( 'plugin' === $threat->extension->type ) { self::handle_extension_threats( $threat, $history, 'plugin' ); continue; } if ( 'theme' === $threat->extension->type ) { self::handle_extension_threats( $threat, $history, 'theme' ); continue; } } if ( 'Vulnerable.WP.Core' === $threat->signature ) { self::handle_core_threats( $threat, $history ); continue; } self::handle_additional_threats( $threat, $history ); } return $history; } /** * Handles threats for extensions such as plugins or themes. * * @phan-suppress PhanDeprecatedProperty -- Maintaining backwards compatibility. * * @param object $threat The threat object. * @param object $history The history object. * @param string $type The type of extension ('plugin' or 'theme'). * @return void */ private static function handle_extension_threats( $threat, $history, $type ) { $extension_list = $type === 'plugin' ? 'plugins' : 'themes'; $extensions = &$history->{ $extension_list}; $found_index = null; // Check if the extension does not exist in the array foreach ( $extensions as $index => $extension ) { if ( $extension->slug === $threat->extension->slug ) { $found_index = $index; break; } } // Add the extension if it does not yet exist in the history if ( $found_index === null ) { $new_extension = new Extension_Model( array( 'name' => $threat->extension->name ?? null, 'slug' => $threat->extension->slug ?? null, 'version' => $threat->extension->version ?? null, 'type' => $type, 'checked' => true, 'threats' => array(), ) ); $extensions[] = $new_extension; $found_index = array_key_last( $extensions ); } // Add the threat to the found extension $extensions[ $found_index ]->threats[] = new Threat_Model( $threat ); // Increment the threat counts ++$history->num_threats; if ( $type === 'plugin' ) { ++$history->num_plugins_threats; } elseif ( $type === 'theme' ) { ++$history->num_themes_threats; } } /** * Handles core threats * * @param object $threat The threat object. * @param object $history The history object. * @return void */ private static function handle_core_threats( $threat, $history ) { // Check if the core version does not exist in the array $found_index = null; foreach ( $history->core as $index => $core ) { if ( $core->version === $threat->version ) { $found_index = $index; break; } } // Add the extension if it does not yet exist in the history if ( null === $found_index ) { $new_core = new Extension_Model( array( 'name' => 'WordPress', 'version' => $threat->version, 'type' => 'core', 'checked' => true, 'threats' => array(), ) ); $history->core[] = $new_core; $found_index = array_key_last( $history->core ); } // Add the threat to the found core $history->core[ $found_index ]->threats[] = new Threat_Model( $threat ); ++$history->num_threats; ++$history->num_core_threats; } /** * Handles additional threats that are not core, plugin or theme * * @param object $threat The threat object. * @param object $history The history object. * @return void */ private static function handle_additional_threats( $threat, $history ) { if ( ! empty( $threat->filename ) ) { $history->files[] = new Threat_Model( $threat ); ++$history->num_threats; } elseif ( ! empty( $threat->table ) ) { $history->database[] = new Threat_Model( $threat ); ++$history->num_threats; } } }