diff --git a/projects/packages/protect-models/changelog/add-protect-fixer-status-to-initial-state b/projects/packages/protect-models/changelog/add-protect-fixer-status-to-initial-state new file mode 100644 index 0000000000000..7b01d76eebc80 --- /dev/null +++ b/projects/packages/protect-models/changelog/add-protect-fixer-status-to-initial-state @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Adds a fixable_threats status property diff --git a/projects/packages/protect-models/src/class-status-model.php b/projects/packages/protect-models/src/class-status-model.php index ae41025e6b331..73bec9dd0f4de 100644 --- a/projects/packages/protect-models/src/class-status-model.php +++ b/projects/packages/protect-models/src/class-status-model.php @@ -53,6 +53,13 @@ class Status_Model { */ public $status; + /** + * List of fixable threat IDs. + * + * @var string[] + */ + public $fixable_threat_ids = array(); + /** * WordPress core status. * diff --git a/projects/packages/protect-status/changelog/add-protect-fixer-status-to-initial-state b/projects/packages/protect-status/changelog/add-protect-fixer-status-to-initial-state new file mode 100644 index 0000000000000..7b01d76eebc80 --- /dev/null +++ b/projects/packages/protect-status/changelog/add-protect-fixer-status-to-initial-state @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Adds a fixable_threats status property diff --git a/projects/packages/protect-status/src/class-scan-status.php b/projects/packages/protect-status/src/class-scan-status.php index d0f3d58e26e75..0ed447f3b8fd3 100644 --- a/projects/packages/protect-status/src/class-scan-status.php +++ b/projects/packages/protect-status/src/class-scan-status.php @@ -169,6 +169,10 @@ private static function normalize_api_data( $scan_data ) { if ( isset( $scan_data->threats ) && is_array( $scan_data->threats ) ) { foreach ( $scan_data->threats as $threat ) { + if ( isset( $threat->fixable ) && $threat->fixable ) { + $status->fixable_threat_ids[] = $threat->id; + } + if ( isset( $threat->extension->type ) ) { if ( 'plugin' === $threat->extension->type ) { // add the extension if it does not yet exist in the status diff --git a/projects/packages/protect-status/tests/php/test-scan-status.php b/projects/packages/protect-status/tests/php/test-scan-status.php index 653d50b460e33..1338590c783f1 100644 --- a/projects/packages/protect-status/tests/php/test-scan-status.php +++ b/projects/packages/protect-status/tests/php/test-scan-status.php @@ -134,6 +134,7 @@ public function get_sample_status() { 'num_plugins_threats' => 1, 'num_themes_threats' => 0, 'status' => 'idle', + 'fixable_threat_ids' => array( '69353714' ), 'plugins' => array( new Extension_Model( array( diff --git a/projects/plugins/protect/changelog/add-protect-fixer-status-to-initial-state b/projects/plugins/protect/changelog/add-protect-fixer-status-to-initial-state new file mode 100644 index 0000000000000..460c195f87dad --- /dev/null +++ b/projects/plugins/protect/changelog/add-protect-fixer-status-to-initial-state @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Adds fixer status to the initial state diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php index 0f50291c34cc6..fad23ee804cb4 100644 --- a/projects/plugins/protect/src/class-jetpack-protect.php +++ b/projects/plugins/protect/src/class-jetpack-protect.php @@ -24,6 +24,7 @@ use Automattic\Jetpack\Protect\REST_Controller; use Automattic\Jetpack\Protect\Scan_History; use Automattic\Jetpack\Protect\Site_Health; +use Automattic\Jetpack\Protect\Threats; use Automattic\Jetpack\Protect_Status\Plan; use Automattic\Jetpack\Protect_Status\Protect_Status; use Automattic\Jetpack\Protect_Status\Scan_Status; @@ -206,12 +207,15 @@ public function initial_state() { global $wp_version; // phpcs:disable WordPress.Security.NonceVerification.Recommended $refresh_status_from_wpcom = isset( $_GET['checkPlan'] ); - $initial_state = array( + $status = Status::get_status( $refresh_status_from_wpcom ); + + $initial_state = array( 'apiRoot' => esc_url_raw( rest_url() ), 'apiNonce' => wp_create_nonce( 'wp_rest' ), 'registrationNonce' => wp_create_nonce( 'jetpack-registration-nonce' ), 'credentials' => Credentials::get_credential_array(), - 'status' => Status::get_status( $refresh_status_from_wpcom ), + 'status' => $status, + 'fixerStatus' => Threats::fix_threats_status( $status->fixable_threat_ids ), 'scanHistory' => Scan_History::get_scan_history( $refresh_status_from_wpcom ), 'installedPlugins' => Plugins_Installer::get_plugins(), 'installedThemes' => Sync_Functions::get_themes(), diff --git a/projects/plugins/protect/src/class-threats.php b/projects/plugins/protect/src/class-threats.php index db71aaa75ef68..11030a4c29ef4 100644 --- a/projects/plugins/protect/src/class-threats.php +++ b/projects/plugins/protect/src/class-threats.php @@ -144,6 +144,10 @@ public static function fix_threats( $threat_ids ) { * @return bool|array */ public static function fix_threats_status( $threat_ids ) { + if ( empty( $threat_ids ) ) { + return false; + } + $api_base = self::get_api_base(); if ( is_wp_error( $api_base ) ) { return false; diff --git a/projects/plugins/protect/src/js/components/notice/styles.module.scss b/projects/plugins/protect/src/js/components/notice/styles.module.scss index 3154fd545f204..1b5a51f3940eb 100644 --- a/projects/plugins/protect/src/js/components/notice/styles.module.scss +++ b/projects/plugins/protect/src/js/components/notice/styles.module.scss @@ -4,6 +4,7 @@ display: flex; border-radius: var( --jp-border-radius ); // 4px overflow: hidden; + z-index: 1; &.notice--info { border-left: 4px solid var( --jp-yellow-20 ); diff --git a/projects/plugins/protect/src/js/components/paid-accordion/index.jsx b/projects/plugins/protect/src/js/components/paid-accordion/index.jsx index fffabbaa138aa..282fe68f38f6b 100644 --- a/projects/plugins/protect/src/js/components/paid-accordion/index.jsx +++ b/projects/plugins/protect/src/js/components/paid-accordion/index.jsx @@ -80,7 +80,7 @@ export const PaidAccordionItem = ( { [ styles[ 'accordion-body-close' ] ]: ! open, } ); - const { fixersStatus } = useFixers(); + const { fixInProgressThreatIds } = useFixers(); const handleClick = useCallback( () => { if ( ! open ) { @@ -122,7 +122,7 @@ export const PaidAccordionItem = ( {
{ fixable && ( <> - { fixersStatus?.threats?.[ id ]?.status === 'in_progress' ? ( + { fixInProgressThreatIds.includes( id ) ? ( ) : ( diff --git a/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx b/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx index 901044e34396b..a1203314acfc8 100644 --- a/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx +++ b/projects/plugins/protect/src/js/components/threats-list/paid-list.jsx @@ -33,8 +33,8 @@ const ThreatAccordionItem = ( { const { setModal } = useModal(); const { recordEvent } = useAnalyticsTracks(); - const { fixersStatus } = useFixers(); - const fixerInProgress = fixersStatus?.threats?.[ id ]?.status === 'in_progress'; + const { fixInProgressThreatIds } = useFixers(); + const fixerInProgress = fixInProgressThreatIds.includes( id ); const learnMoreButton = source ? ( @@ -156,6 +155,7 @@ const ThreatAccordionItem = ( { isDestructive={ true } variant="secondary" onClick={ handleIgnoreThreatClick() } + disabled={ fixerInProgress } > { __( 'Ignore threat', 'jetpack-protect' ) } diff --git a/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts b/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts index 791555f1b16ca..2f5e52d748294 100644 --- a/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts +++ b/projects/plugins/protect/src/js/data/scan/use-fixers-query.ts @@ -1,6 +1,7 @@ import { useConnection } from '@automattic/jetpack-connection'; import { useQuery, useQueryClient, type UseQueryResult } from '@tanstack/react-query'; import { __ } from '@wordpress/i18n'; +import { useEffect, useMemo } from 'react'; import API from '../../api'; import { QUERY_FIXERS_KEY, QUERY_HISTORY_KEY, QUERY_SCAN_STATUS_KEY } from '../../constants'; import useNotices from '../../hooks/use-notices'; @@ -31,12 +32,23 @@ export default function useFixersQuery( { skipUserConnection: true, } ); - return useQuery( { + // Memoize initialData to prevent recalculating on every render + const initialData: FixersStatus = useMemo( + () => + window.jetpackProtectInitialState?.fixerStatus || { + ok: true, + threats: {}, + }, + [] + ); + + const fixersQuery = useQuery( { queryKey: [ QUERY_FIXERS_KEY ], queryFn: async () => { + // Fetch fixer status from API const data = await API.getFixersStatus( threatIds ); const cachedData = queryClient.getQueryData( [ QUERY_FIXERS_KEY ] ) as - | { threats: object } + | FixersStatus | undefined; // Check if any fixers have completed, by comparing the latest data against the cache. @@ -63,8 +75,10 @@ export default function useFixersQuery( { } } ); + // Return the fetched data so the query resolves return data; }, + retry: false, refetchInterval( query ) { if ( ! usePolling || ! query.state.data ) { return false; @@ -82,7 +96,20 @@ export default function useFixersQuery( { return false; }, - initialData: { threats: {} }, // to do: provide initial data in window.jetpackProtectInitialState + initialData: initialData, enabled: isRegistered, } ); + + // Handle error if present in the query result + useEffect( () => { + if ( fixersQuery.isError && fixersQuery.error ) { + // Reset the query data to initial state + queryClient.setQueryData( [ QUERY_FIXERS_KEY ], initialData ); + + // Show an error notice + showErrorNotice( __( 'An error occurred while fetching fixers status.', 'jetpack-protect' ) ); + } + }, [ fixersQuery.isError, fixersQuery.error, queryClient, initialData, showErrorNotice ] ); + + return fixersQuery; } diff --git a/projects/plugins/protect/src/js/hooks/use-fixers.ts b/projects/plugins/protect/src/js/hooks/use-fixers.ts index 6c7c8062e98f1..ba4f6ca0ea2dd 100644 --- a/projects/plugins/protect/src/js/hooks/use-fixers.ts +++ b/projects/plugins/protect/src/js/hooks/use-fixers.ts @@ -3,10 +3,10 @@ import useFixersMutation from '../data/scan/use-fixers-mutation'; import useFixersQuery from '../data/scan/use-fixers-query'; import useScanStatusQuery from '../data/scan/use-scan-status-query'; import { FixersStatus } from '../types/fixers'; -import { Threat } from '../types/threats'; type UseFixersResult = { - fixableThreats: Threat[]; + fixableThreatIds: number[]; + fixInProgressThreatIds: number[]; fixersStatus: FixersStatus; fixThreats: ( threatIds: number[] ) => Promise< unknown >; isLoading: boolean; @@ -20,26 +20,25 @@ type UseFixersResult = { export default function useFixers(): UseFixersResult { const { data: status } = useScanStatusQuery(); const fixersMutation = useFixersMutation(); - - const fixableThreats = useMemo( () => { - const threats = [ - ...( status?.core?.threats || [] ), - ...( status?.plugins?.map( plugin => plugin.threats ).flat() || [] ), - ...( status?.themes?.map( theme => theme.threats ).flat() || [] ), - ...( status?.files || [] ), - ...( status?.database || [] ), - ]; - - return threats.filter( threat => threat.fixable ); - }, [ status ] ); - const { data: fixersStatus } = useFixersQuery( { - threatIds: fixableThreats.map( threat => threat.id ), + threatIds: status.fixableThreatIds, usePolling: true, } ); + // List of threat IDs that are currently being fixed. + const fixInProgressThreatIds = useMemo( + () => + Object.entries( fixersStatus?.threats || {} ) + .filter( + ( [ , threat ]: [ string, { status?: string } ] ) => threat.status === 'in_progress' + ) + .map( ( [ id ] ) => parseInt( id ) ), + [ fixersStatus ] + ); + return { - fixableThreats, + fixableThreatIds: status.fixableThreatIds, + fixInProgressThreatIds, fixersStatus, fixThreats: fixersMutation.mutateAsync, isLoading: fixersMutation.isPending, diff --git a/projects/plugins/protect/src/js/types/global.d.ts b/projects/plugins/protect/src/js/types/global.d.ts index 5fc0a2444334d..d844820616d06 100644 --- a/projects/plugins/protect/src/js/types/global.d.ts +++ b/projects/plugins/protect/src/js/types/global.d.ts @@ -14,6 +14,7 @@ declare global { registrationNonce: string; credentials: [ Record< string, unknown > ]; status: ScanStatus; + fixerStatus: FixersStatus; scanHistory: ScanStatus; installedPlugins: { [ key: string ]: PluginData; diff --git a/projects/plugins/protect/src/js/types/scans.ts b/projects/plugins/protect/src/js/types/scans.ts index 665062afbfebd..7e76cd8be7ab9 100644 --- a/projects/plugins/protect/src/js/types/scans.ts +++ b/projects/plugins/protect/src/js/types/scans.ts @@ -24,6 +24,9 @@ export type ScanStatus = { /** The current status of the scanner. */ status: 'unavailable' | 'provisioning' | 'idle' | 'scanning' | 'scheduled'; + /** The IDs of fixable threats. */ + fixableThreatIds: number[]; + /** The current scan progress, only available from the Scan API. */ current_progress: number | null;