From 21f57029c8e93b54775f02a114216452aed44fcc Mon Sep 17 00:00:00 2001
From: dkmyta <43220201+dkmyta@users.noreply.github.com>
Date: Fri, 28 Jun 2024 09:35:19 -0700
Subject: [PATCH] Protect: Add Scan History UI (#37988)
---
.../changelog/add-protect-scan-history-ui-alt | 4 +
.../protect/src/class-jetpack-protect.php | 1 +
.../protect/src/class-rest-controller.php | 51 +++
.../protect/src/class-scan-history.php | 14 +
.../plugins/protect/src/class-threats.php | 4 +
projects/plugins/protect/src/js/api.js | 7 +
.../js/components/paid-accordion/index.jsx | 55 ++-
.../paid-accordion/styles.module.scss | 58 ++-
.../src/js/components/scan-page/index.jsx | 339 +++++++++++-------
.../components/scan-page/styles.module.scss | 6 +
.../src/js/components/summary/index.jsx | 121 +++++--
.../js/components/summary/styles.module.scss | 4 +-
.../src/js/components/threats-list/empty.jsx | 31 +-
.../src/js/components/threats-list/index.jsx | 21 +-
.../js/components/threats-list/navigation.jsx | 1 +
.../js/components/threats-list/paid-list.jsx | 28 +-
.../threats-list/use-threats-list.js | 1 -
.../src/js/hooks/use-protect-data/index.js | 55 +--
.../src/js/hooks/use-scan-history/index.js | 74 ++++
.../plugins/protect/src/js/state/actions.js | 14 +
.../plugins/protect/src/js/state/reducers.js | 20 ++
.../plugins/protect/src/js/state/selectors.js | 2 +
.../protect/src/models/class-threat-model.php | 122 +++++++
23 files changed, 822 insertions(+), 211 deletions(-)
create mode 100644 projects/plugins/protect/changelog/add-protect-scan-history-ui-alt
create mode 100644 projects/plugins/protect/src/js/hooks/use-scan-history/index.js
create mode 100644 projects/plugins/protect/src/models/class-threat-model.php
diff --git a/projects/plugins/protect/changelog/add-protect-scan-history-ui-alt b/projects/plugins/protect/changelog/add-protect-scan-history-ui-alt
new file mode 100644
index 0000000000000..557cbed04c102
--- /dev/null
+++ b/projects/plugins/protect/changelog/add-protect-scan-history-ui-alt
@@ -0,0 +1,4 @@
+Significance: minor
+Type: added
+
+Add Scan History UI
diff --git a/projects/plugins/protect/src/class-jetpack-protect.php b/projects/plugins/protect/src/class-jetpack-protect.php
index 26730756a952c..854978215ee46 100644
--- a/projects/plugins/protect/src/class-jetpack-protect.php
+++ b/projects/plugins/protect/src/class-jetpack-protect.php
@@ -212,6 +212,7 @@ public function initial_state() {
'apiNonce' => wp_create_nonce( 'wp_rest' ),
'registrationNonce' => wp_create_nonce( 'jetpack-registration-nonce' ),
'status' => Status::get_status( $refresh_status_from_wpcom ),
+ 'viewingScanHistory' => false,
'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-rest-controller.php b/projects/plugins/protect/src/class-rest-controller.php
index 6e082b5a8bd68..0e11a3d08bd24 100644
--- a/projects/plugins/protect/src/class-rest-controller.php
+++ b/projects/plugins/protect/src/class-rest-controller.php
@@ -198,6 +198,30 @@ public static function register_rest_endpoints() {
},
)
);
+
+ register_rest_route(
+ 'jetpack-protect/v1',
+ 'scan-history',
+ array(
+ 'methods' => \WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::api_get_scan_history',
+ 'permission_callback' => function () {
+ return current_user_can( 'manage_options' );
+ },
+ )
+ );
+
+ register_rest_route(
+ 'jetpack-protect/v1',
+ 'clear-scan-history-cache',
+ array(
+ 'methods' => \WP_REST_Server::EDITABLE,
+ 'callback' => __CLASS__ . '::api_clear_scan_history_cache',
+ 'permission_callback' => function () {
+ return current_user_can( 'manage_options' );
+ },
+ )
+ );
}
/**
@@ -409,4 +433,31 @@ public static function api_complete_onboarding_steps( $request ) {
return new WP_REST_Response( 'Onboarding step(s) completed.' );
}
+
+ /**
+ * Return Scan History for the API endpoint
+ *
+ * @param WP_REST_Request $request The request object.
+ *
+ * @return WP_REST_Response
+ */
+ public static function api_get_scan_history( $request ) {
+ $scan_history = Scan_History::get_scan_history( false, $request['filter'] );
+ return rest_ensure_response( $scan_history, 200 );
+ }
+
+ /**
+ * Clear the Scan_History cache for the API endpoint
+ *
+ * @return WP_REST_Response
+ */
+ public static function api_clear_scan_history_cache() {
+ $cache_cleared = Scan_History::delete_option();
+
+ if ( ! $cache_cleared ) {
+ return new WP_REST_Response( 'An error occured while attempting to clear the Jetpack Scan history cache.', 500 );
+ }
+
+ return new WP_REST_Response( 'Jetpack Scan history cache cleared.' );
+ }
}
diff --git a/projects/plugins/protect/src/class-scan-history.php b/projects/plugins/protect/src/class-scan-history.php
index 9636dfb6404d1..b671c83c30bf0 100644
--- a/projects/plugins/protect/src/class-scan-history.php
+++ b/projects/plugins/protect/src/class-scan-history.php
@@ -9,6 +9,8 @@
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
+use Automattic\Jetpack\Protect_Models\Extension_Model;
+use Automattic\Jetpack\Protect_Status\Plan;
use Jetpack_Options;
use WP_Error;
@@ -96,6 +98,18 @@ public static function update_history_option( $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
*
diff --git a/projects/plugins/protect/src/class-threats.php b/projects/plugins/protect/src/class-threats.php
index 0f77d1bc33523..63a20f9ef3c89 100644
--- a/projects/plugins/protect/src/class-threats.php
+++ b/projects/plugins/protect/src/class-threats.php
@@ -65,6 +65,7 @@ public static function update_threat( $threat_id, $updates ) {
// clear the now out-of-date cache
Scan_Status::delete_option();
+ Scan_History::delete_option();
return true;
}
@@ -113,6 +114,7 @@ public static function fix_threats( $threat_ids ) {
// clear the now out-of-date cache
Scan_Status::delete_option();
+ Scan_History::delete_option();
$parsed_response = json_decode( $response['body'] );
@@ -158,6 +160,7 @@ public static function fix_threats_status( $threat_ids ) {
// clear the potentially out-of-date cache
Scan_Status::delete_option();
+ Scan_History::delete_option();
return $parsed_response;
}
@@ -197,6 +200,7 @@ public static function scan() {
// clear the now out-of-date cache
Scan_Status::delete_option();
+ Scan_History::delete_option();
return true;
}
diff --git a/projects/plugins/protect/src/js/api.js b/projects/plugins/protect/src/js/api.js
index 4b5afaedc2877..3b75d4246b604 100644
--- a/projects/plugins/protect/src/js/api.js
+++ b/projects/plugins/protect/src/js/api.js
@@ -45,6 +45,13 @@ const API = {
method: 'POST',
data: { step_ids: stepIds },
} ),
+
+ fetchScanHistory: $filter =>
+ apiFetch( {
+ path: 'jetpack-protect/v1/scan-history',
+ method: 'POST',
+ data: { filter: $filter },
+ } ),
};
export default API;
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 ef0a38d91932b..1839ef6c26a8b 100644
--- a/projects/plugins/protect/src/js/components/paid-accordion/index.jsx
+++ b/projects/plugins/protect/src/js/components/paid-accordion/index.jsx
@@ -1,9 +1,11 @@
import { Spinner, Text, useBreakpointMatch } from '@automattic/jetpack-components';
import { useSelect } from '@wordpress/data';
-import { __ } from '@wordpress/i18n';
+import { dateI18n } from '@wordpress/date';
+import { sprintf, __ } from '@wordpress/i18n';
import { Icon, check, chevronDown, chevronUp } from '@wordpress/icons';
import clsx from 'clsx';
import React, { useState, useCallback, useContext } from 'react';
+import useScanHistory from '../../hooks/use-scan-history';
import { STORE_ID } from '../../state/store';
import ThreatSeverityBadge from '../severity';
import styles from './styles.module.scss';
@@ -18,12 +20,15 @@ export const PaidAccordionItem = ( {
fixable,
severity,
children,
+ firstDetected,
+ fixedOn,
onOpen,
} ) => {
const accordionData = useContext( PaidAccordionContext );
const open = accordionData?.open === id;
const setOpen = accordionData?.setOpen;
const threatsAreFixing = useSelect( select => select( STORE_ID ).getThreatsAreFixing() );
+ const { viewingScanHistory } = useScanHistory();
const bodyClassNames = clsx( styles[ 'accordion-body' ], {
[ styles[ 'accordion-body-open' ] ]: open,
@@ -41,6 +46,49 @@ export const PaidAccordionItem = ( {
const [ isSmall ] = useBreakpointMatch( [ 'sm', 'lg' ], [ null, '<' ] );
+ const FixDetails = ( { date, isFixed } ) => (
+
+ { isFixed
+ ? sprintf(
+ /* translators: %s: Fixed on date */
+ __( 'Threat fixed %s', 'jetpack-protect' ),
+ dateI18n( 'M j, Y', date )
+ )
+ : __( 'Threat ignored', 'jetpack-protect' ) }
+
+ );
+
+ const ScanHistoryDetails = ( { viewingHistory, detectedAt, fixedAt } ) => {
+ if ( ! viewingHistory ) {
+ return null;
+ }
+
+ return (
+ <>
+ { detectedAt && (
+