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;